First commit: Tryton client using TypeScript

This commit is contained in:
2025-10-10 10:22:11 -05:00
commit edda76cfc8
14 changed files with 3523 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
# Node.js
node_modules/
# Build output
dist/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE/Editor
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db
# TypeScript
*.tsbuildinfo
# Env files
.env
.env.*
# Others
*.log

31
.npmignore Normal file
View File

@@ -0,0 +1,31 @@
# Archivos de desarrollo
*.ts
tsconfig.json
.vscode
.idea
.DS_Store
# Archivos de Node.js
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Tests
*.test.ts
*.spec.ts
test/
tests/
__tests__/
coverage/
# Build temporales
*.tsbuildinfo
.cache
# Otros
.git
.gitignore
.npmignore
*.md.backup

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

390
README.md Normal file
View File

@@ -0,0 +1,390 @@
# Tryton RPC Client para Node.js
Cliente TypeScript completo para conectar con servidores Tryton ERP a través de JSON-RPC.
## 🚀 Características
-**100% TypeScript** - Tipado completo y seguro
-**Compatible con Node.js** - Versión 14 o superior
-**Operaciones CRUD** - Create, Read, Update, Delete
-**Búsquedas avanzadas** - Search, SearchRead, SearchCount
-**Cache integrado** - Sistema LRU cache para optimización
-**Pool de conexiones** - Manejo eficiente de múltiples conexiones
-**Soporte HTTPS/HTTP** - Conexiones seguras
-**Operaciones tipadas** - Factory para modelos con tipos específicos
## 📦 Instalación
### Opción 1: Instalación local (sin npm)
Simplemente copia la carpeta completa a tu proyecto y construye:
```bash
cd trytonClientNode
npm install
npm run build
```
### Opción 2: Usar directamente en otro proyecto
Desde la carpeta de tu proyecto:
```bash
npm install /ruta/a/trytonClientNode
```
## 🔧 Uso
### Conexión básica
```typescript
import { TrytonClient } from '@tryton/client-node';
// Crear cliente
const client = new TrytonClient({
hostname: 'localhost',
port: 8000,
database: 'tryton_db',
username: 'admin',
password: 'admin',
language: 'es'
});
// Conectar
await client.connect();
```
### Operaciones CRUD
#### Leer registros
```typescript
// Leer registros específicos
const parties = await client.read(
'party.party',
[1, 2, 3],
['name', 'code', 'email']
);
console.log(parties);
// [{ id: 1, name: 'Company A', code: 'C001', email: 'a@example.com' }, ...]
```
#### Crear registros
```typescript
// Crear nuevos registros
const ids = await client.create('party.party', [
{ name: 'New Company', code: 'NC001' },
{ name: 'Another Company', code: 'AC001' }
]);
console.log(ids); // [4, 5]
```
#### Actualizar registros
```typescript
// Actualizar registros existentes
await client.write(
'party.party',
[1, 2],
{ active: false }
);
```
#### Eliminar registros
```typescript
// Eliminar registros
await client.delete('party.party', [4, 5]);
```
### Búsquedas
#### Search - Buscar IDs
```typescript
// Buscar IDs que cumplan criterios
const ids = await client.search(
'party.party',
[['name', 'like', '%Company%']],
0, // offset
10, // limit
['name'] // order
);
console.log(ids); // [1, 2, 3]
```
#### SearchRead - Buscar y leer en una sola operación
```typescript
// Buscar y leer directamente
const parties = await client.searchRead(
'party.party',
[['active', '=', true]],
['name', 'code', 'email'],
0,
100
);
```
#### SearchCount - Contar registros
```typescript
// Contar registros que cumplen criterios
const count = await client.searchCount(
'party.party',
[['active', '=', true]]
);
console.log(count); // 150
```
### Operaciones tipadas (Type-safe)
```typescript
// Definir interfaz del modelo
interface Party {
id: number;
name: string;
code: string;
email?: string;
active: boolean;
}
// Usar operaciones tipadas
const partyModel = client.model<Party>('party.party');
// Todas las operaciones están tipadas
const parties = await partyModel.searchRead(
[['active', '=', true]],
['name', 'code', 'email']
);
// TypeScript sabe que 'parties' es Party[]
parties.forEach(party => {
console.log(party.name); // ✅ Autocompletado
});
```
### Métodos de utilidad
```typescript
// Obtener información del usuario actual
const user = await client.getCurrentUser();
console.log(user.name, user.login);
// Obtener preferencias del usuario
const prefs = await client.getUserPreferences();
console.log(prefs.language);
// Obtener versión del servidor
const version = await client.getVersion();
console.log(version);
// Listar bases de datos disponibles
const databases = await client.listDatabases();
console.log(databases);
```
### Llamadas RPC personalizadas
```typescript
// Llamar a cualquier método RPC
const result = await client.call('model.party.party.custom_method', [
arg1,
arg2,
{ context: 'value' }
]);
```
### Manejo de cache
```typescript
// Limpiar cache completo
client.clearCache();
// Limpiar cache con prefijo específico
client.clearCache('model.party.party');
```
## 📋 API Principal
### Constructor
```typescript
new TrytonClient(config: TrytonClientConfig)
```
**TrytonClientConfig:**
- `hostname` - Host del servidor (con o sin `http://`/`https://`)
- `database` - Nombre de la base de datos
- `username` - Usuario de Tryton
- `password` - Contraseña
- `port` - Puerto del servidor (default: 8000)
- `language` - Idioma (default: 'en')
- `options` - Opciones adicionales (cache, timeouts, etc.)
### Métodos principales
| Método | Descripción |
|--------|-------------|
| `connect()` | Conecta y autentica con el servidor |
| `read(model, ids, fields, context?)` | Lee registros específicos |
| `create(model, records, context?)` | Crea nuevos registros |
| `write(model, ids, values, context?)` | Actualiza registros |
| `delete(model, ids, context?)` | Elimina registros |
| `search(model, domain, offset?, limit?, order?, context?)` | Busca IDs |
| `searchRead(model, domain, fields, offset?, limit?, order?, context?)` | Busca y lee |
| `searchCount(model, domain, context?)` | Cuenta registros |
| `call(method, args?)` | Llamada RPC genérica |
| `model(modelName)` | Factory de operaciones tipadas |
### Propiedades
| Propiedad | Descripción |
|-----------|-------------|
| `isConnected` | Indica si está conectado |
| `ssl` | Indica si usa HTTPS |
| `url` | URL de conexión |
## 🔒 Dominios de búsqueda
Los dominios en Tryton siguen la sintaxis de Python:
```typescript
// Operadores básicos
[['field', '=', value]] // Igual
[['field', '!=', value]] // Diferente
[['field', '>', value]] // Mayor que
[['field', '<', value]] // Menor que
[['field', '>=', value]] // Mayor o igual
[['field', '<=', value]] // Menor o igual
[['field', 'like', '%pattern%']] // Like (SQL)
[['field', 'in', [1, 2, 3]]] // En lista
// Operadores lógicos
['OR', [['f1', '=', 'a']], [['f2', '=', 'b']]] // OR
['AND', [['f1', '=', 'a']], [['f2', '=', 'b']]] // AND (default)
// Ejemplo complejo
[
'OR',
['AND', [['name', 'like', '%Inc%']], [['active', '=', true]]],
[['code', 'in', ['A001', 'B002']]]
]
```
## 🛠️ Scripts de desarrollo
```bash
# Compilar el código TypeScript
npm run build
# Compilar en modo watch
npm run build:watch
# Limpiar archivos compilados
npm run clean
```
## 📁 Estructura de archivos
```
trytonClientNode/
├── cache.ts # Sistema de cache LRU
├── client.ts # Cliente principal
├── index.ts # Exports públicos
├── jsonrpc.ts # Implementación JSON-RPC
├── types.ts # Definiciones de tipos
├── package.json # Configuración del paquete
├── tsconfig.json # Configuración TypeScript
└── README.md # Este archivo
```
## 🔍 Ejemplo completo
```typescript
import { TrytonClient } from '@tryton/client-node';
async function main() {
// 1. Crear y conectar
const client = new TrytonClient({
hostname: 'http://localhost',
port: 8000,
database: 'tryton',
username: 'admin',
password: 'admin'
});
await client.connect();
console.log('✅ Conectado');
// 2. Buscar empresas activas
const activeParties = await client.searchRead(
'party.party',
[['active', '=', true]],
['name', 'code'],
0,
10
);
console.log('Empresas activas:', activeParties);
// 3. Crear una nueva empresa
const [newId] = await client.create('party.party', [{
name: 'Mi Nueva Empresa',
code: 'MNE001'
}]);
console.log('✅ Empresa creada con ID:', newId);
// 4. Leer la empresa creada
const [newParty] = await client.read(
'party.party',
[newId],
['name', 'code', 'create_date']
);
console.log('Empresa creada:', newParty);
// 5. Actualizar la empresa
await client.write(
'party.party',
[newId],
{ name: 'Empresa Actualizada' }
);
console.log('✅ Empresa actualizada');
// 6. Verificar actualización
const [updated] = await client.read(
'party.party',
[newId],
['name']
);
console.log('Nuevo nombre:', updated.name);
}
main().catch(console.error);
```
## ⚠️ Requisitos
- Node.js >= 14.0.0
- Servidor Tryton activo y accesible
## 📝 Licencia
MIT
## 🤝 Contribuciones
Este es un paquete independiente. Puedes modificarlo y adaptarlo según tus necesidades.
## 📧 Soporte
Para problemas o preguntas, revisa la documentación oficial de Tryton en https://www.tryton.org/

297
cache.ts Normal file
View File

@@ -0,0 +1,297 @@
/**
* Cache system similar to Python's CacheDict from Tryton
* Implements LRU (Least Recently Used) cache using JavaScript Map
* TypeScript version
*/
export type CacheFactory<T> = () => T;
export class CacheDict<K = any, V = any> extends Map<K, V> {
private cacheLen: number;
private defaultFactory: CacheFactory<V> | null;
/**
* Create a new CacheDict
*/
constructor(
cacheLen: number = 10,
defaultFactory: CacheFactory<V> | null = null
) {
super();
this.cacheLen = cacheLen;
this.defaultFactory = defaultFactory;
}
/**
* Set a key-value pair and maintain LRU order
*/
override set(key: K, value: V): this {
// If key exists, delete it first to move to end
if (this.has(key)) {
this.delete(key);
}
super.set(key, value);
// Remove oldest entries if cache is full
while (this.size > this.cacheLen) {
const firstKey = this.keys().next().value;
if (firstKey !== undefined) {
this.delete(firstKey);
}
}
return this;
}
/**
* Get a value and move it to end (most recently used)
*/
override get(key: K): V | undefined {
if (this.has(key)) {
const value = super.get(key)!;
// Move to end by re-setting
this.delete(key);
super.set(key, value);
return value;
}
// Handle missing key with default factory
if (this.defaultFactory) {
const value = this.defaultFactory();
this.set(key, value);
return value;
}
return undefined;
}
/**
* Override has() to update LRU order on access
*/
override has(key: K): boolean {
const exists = super.has(key);
if (exists) {
// Move to end on access
const value = super.get(key)!;
this.delete(key);
super.set(key, value);
}
return exists;
}
/**
* Get current cache size
*/
get length(): number {
return this.size;
}
/**
* Clear all items from cache
*/
override clear(): void {
super.clear();
}
/**
* Convert cache to array for debugging
*/
toArray(): Array<[K, V]> {
return Array.from(this.entries());
}
}
export interface CacheEntry<T = any> {
expire: Date;
value: T;
}
export interface CacheStats {
totalPrefixes: number;
totalEntries: number;
prefixes: Record<string, number>;
}
/**
* Advanced cache for Tryton RPC with expiration support
*/
export class TrytonCache {
private store: CacheDict<string, CacheDict<string, CacheEntry>>;
private cacheLen: number;
constructor(cacheLen: number = 1024) {
this.cacheLen = cacheLen;
this.store = new CacheDict<string, CacheDict<string, CacheEntry>>(
cacheLen,
() => new CacheDict<string, CacheEntry>(cacheLen)
);
}
/**
* Check if a prefix is cached
*/
cached(prefix: string): boolean {
return this.store.has(prefix);
}
/**
* Set cache entry with expiration
*/
set(prefix: string, key: string, expire: number | Date, value: any): void {
let expiration: Date;
if (typeof expire === "number") {
// Assume seconds, convert to Date
expiration = new Date(Date.now() + expire * 1000);
} else if (expire instanceof Date) {
expiration = expire;
} else {
throw new Error("Invalid expiration type");
}
// Deep copy value to avoid mutations
const cachedValue = this._deepCopy(value);
this.store.get(prefix)!.set(key, {
expire: expiration,
value: cachedValue,
});
}
/**
* Get cached value if not expired
*/
get(prefix: string, key: string): any {
const now = new Date();
if (!this.store.has(prefix)) {
throw new Error("Key not found");
}
const prefixCache = this.store.get(prefix)!;
if (!prefixCache.has(key)) {
throw new Error("Key not found");
}
const entry = prefixCache.get(key)!;
if (entry.expire < now) {
prefixCache.delete(key);
throw new Error("Key expired");
}
console.log(`(cached) ${prefix} ${key}`);
return this._deepCopy(entry.value);
}
/**
* Clear cache for a specific prefix or all
*/
clear(prefix?: string): void {
if (prefix) {
if (this.store.has(prefix)) {
this.store.get(prefix)!.clear();
}
} else {
this.store.clear();
}
}
/**
* Deep copy objects to prevent mutations
*/
private _deepCopy<T>(obj: T): T {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as T;
}
if (obj instanceof Array) {
return obj.map((item) => this._deepCopy(item)) as T;
}
// Handle Buffer in Node.js environment
if (typeof Buffer !== "undefined" && obj instanceof Buffer) {
return Buffer.from(obj) as T;
}
if (typeof obj === "object") {
const copy: any = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = this._deepCopy((obj as any)[key]);
}
}
return copy as T;
}
return obj;
}
/**
* Get cache statistics
*/
getStats(): CacheStats {
const stats: CacheStats = {
totalPrefixes: this.store.size,
totalEntries: 0,
prefixes: {},
};
for (const [prefix, prefixCache] of this.store.entries()) {
const count = prefixCache.size;
stats.totalEntries += count;
stats.prefixes[prefix] = count;
}
return stats;
}
/**
* Remove expired entries
*/
cleanupExpired(): number {
const now = new Date();
let removedCount = 0;
for (const [prefix, prefixCache] of this.store.entries()) {
const keysToRemove: string[] = [];
for (const [key, entry] of prefixCache.entries()) {
if (entry.expire < now) {
keysToRemove.push(key);
}
}
keysToRemove.forEach((key) => {
prefixCache.delete(key);
removedCount++;
});
}
return removedCount;
}
/**
* Get cache size in bytes (approximate)
*/
getSizeEstimate(): number {
let size = 0;
for (const [prefix, prefixCache] of this.store.entries()) {
size += prefix.length * 2; // UTF-16 encoding
for (const [key, entry] of prefixCache.entries()) {
size += key.length * 2;
size += JSON.stringify(entry.value).length * 2;
size += 24; // Date object overhead
}
}
return size;
}
}

577
client.ts Normal file
View File

@@ -0,0 +1,577 @@
/**
* 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<boolean>} - True if connection successful
* @throws {Error} - If connection or authentication fails
*/
async connect(): Promise<boolean> {
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<LoginResult>("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<T = any>(methodName: string, args: any[] = []): Promise<T> {
if (!this.connection) {
throw new Error("Not connected. Call connect() first.");
}
return this.connection.withConnection(async (conn) => {
return conn.request<T>(methodName, args);
});
}
/**
* Call multiple RPC methods in sequence
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callMultiple(calls: TrytonMethodCall[]): Promise<any[]> {
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>} - Array of results
*/
async callParallel(calls: TrytonMethodCall[]): Promise<any[]> {
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<number>} ids - Record IDs to read
* @param {Array<string>} fields - Fields to read
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async read<T extends TrytonRecord = TrytonRecord>(
model: ModelName,
ids: RecordIds,
fields: FieldName[],
context: TrytonContext = {}
): Promise<T[]> {
return this.call<T[]>(`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<Object>} records - Records to create
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of created record IDs
*/
async create(
model: ModelName,
records: Record<string, any>[],
context: TrytonContext = {}
): Promise<RecordIds> {
return this.call<RecordIds>(`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<number>} ids - Record IDs to update
* @param {Object} values - Values to update
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async write(
model: ModelName,
ids: RecordIds,
values: Record<string, any>,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`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<number>} ids - Record IDs to delete
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async delete(
model: ModelName,
ids: RecordIds,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`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<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of record IDs
*/
async search(
model: ModelName,
domain: SearchDomain,
offset: number = 0,
limit: number | null = null,
order: string[] | null = null,
context: TrytonContext = {}
): Promise<RecordIds> {
return this.call<RecordIds>(`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<string>} fields - Fields to read
* @param {number} [offset=0] - Offset for pagination
* @param {number} [limit=null] - Limit for pagination
* @param {Array<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async searchRead<T extends TrytonRecord = TrytonRecord>(
model: ModelName,
domain: SearchDomain,
fields: FieldName[],
offset: number = 0,
limit: number | null = null,
order: string[] | null = null,
context: TrytonContext = {}
): Promise<T[]> {
return this.call<T[]>(`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>} - Number of records
*/
async searchCount(
model: ModelName,
domain: SearchDomain,
context: TrytonContext = {}
): Promise<number> {
return this.call<number>(`model.${model}.search_count`, [
domain,
context,
]);
}
/**
* Get database information
* @returns {Promise<Object>} - Database info
*/
async getDatabaseInfo(): Promise<any> {
return this.call("common.db.get_info", []);
}
/**
* List available databases
* @returns {Promise<Array<string>>} - Database names
*/
async listDatabases(): Promise<string[]> {
return this.call<string[]>("common.db.list", []);
}
/**
* Get server version
* @returns {Promise<string>} - Server version
*/
async getVersion(): Promise<string> {
return this.call<string>("common.version", []);
}
/**
* Get user preferences
*/
async getUserPreferences(): Promise<UserPreferences> {
return this.call<UserPreferences>("model.res.user.get_preferences", [
true,
{},
]);
}
/**
* Get current user information
*/
async getCurrentUser(): Promise<TrytonUser> {
const preferences = await this.getUserPreferences();
const users = await this.read<TrytonUser>(
"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<T extends TrytonRecord = TrytonRecord>(
modelName: ModelName
): TypedModelOperations<T> {
return {
read: (
ids: RecordIds,
fields: FieldName[],
context?: TrytonContext
) => this.read<T>(modelName, ids, fields, context),
create: (
records: Partial<Omit<T, "id">>[],
context?: TrytonContext
) =>
this.create(
modelName,
records as Record<string, any>[],
context
),
write: (
ids: RecordIds,
values: Partial<Omit<T, "id">>,
context?: TrytonContext
) =>
this.write(
modelName,
ids,
values as Record<string, any>,
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<T>(
modelName,
domain,
fields,
offset,
limit,
order,
context
),
searchCount: (domain: SearchDomain, context?: TrytonContext) =>
this.searchCount(modelName, domain, context),
};
}
}
// Export for CommonJS compatibility
export default TrytonClient;

140
examples/README.md Normal file
View File

@@ -0,0 +1,140 @@
# Ejemplos de uso del Cliente Tryton
Esta carpeta contiene ejemplos prácticos de cómo utilizar el cliente RPC de Tryton en Node.js.
## Preparación
Antes de ejecutar cualquier ejemplo, asegúrate de tener:
1. **Node.js instalado** (versión 14 o superior)
2. **Servidor Tryton ejecutándose** y accesible
3. **Credenciales válidas** para conectarse al servidor
4. **El proyecto compilado**:
```bash
npm install
npm run build
```
## Ejemplos disponibles
### 1. Conexión Básica (`basic-connection.ts`)
**Propósito**: Demuestra cómo conectarse a Tryton y realizar operaciones básicas.
**Características**:
- ✅ Conexión al servidor Tryton
- ✅ Autenticación de usuario
- ✅ Obtención de información del usuario y token de sesión
- ✅ Búsqueda de terceros (party.party)
- ✅ Manejo de errores y validación de configuración
**Configuración requerida**:
Edita el archivo `basic-connection.ts` y completa estos campos:
```typescript
const config = {
hostname: "localhost", // IP/dominio del servidor Tryton
port: 8000, // Puerto (generalmente 8000)
database: "tu_base_datos", // Nombre de la base de datos
username: "tu_usuario", // Usuario de Tryton
password: "tu_contraseña", // Contraseña
language: "es", // Idioma preferido
// ... resto de opciones
};
```
**Ejecución**:
```bash
# Opción 1: Usando el script npm (recomendado)
npm run example:basic
# Opción 2: Directamente con Node.js
node dist/examples/basic-connection.js
```
**Salida esperada**:
```
🚀 Iniciando ejemplo de conexión con Tryton...
📡 Creando cliente Tryton...
🔗 Conectando al servidor...
✅ Conexión exitosa!
👤 INFORMACIÓN DEL USUARIO
========================================
Usuario: Juan Pérez (ID: 123)
Idioma: es
Token de sesión: usuario:123:abc123xyz789
🏢 LISTA DE TERCEROS
========================================
Buscando terceros...
✅ Se encontraron 15 terceros:
1. ACME Corporation (ACME001)
2. Beta Industries
3. Gamma Solutions (GAM001)
...
🎉 ¡Ejemplo completado exitosamente!
```
## Solución de problemas comunes
### Error de conexión
- **Síntoma**: `Error durante la ejecución: Connection refused`
- **Solución**: Verifica que el servidor Tryton esté ejecutándose en la IP y puerto correctos
### Error de autenticación
- **Síntoma**: `Error durante la ejecución: Invalid login`
- **Solución**: Confirma que el usuario, contraseña y base de datos sean correctos
### Error de base de datos
- **Síntoma**: `Error durante la ejecución: Database not found`
- **Solución**: Asegúrate de que el nombre de la base de datos sea exacto (sensible a mayúsculas)
### Error de permisos
- **Síntoma**: `Access denied for model party.party`
- **Solución**: El usuario necesita permisos de lectura para el modelo de terceros
### Error de compilación TypeScript
- **Síntoma**: Errores durante `npm run build`
- **Solución**: Ejecuta `npm run clean` y luego `npm run build`
## Estructura de archivos
```
examples/
├── README.md # Esta documentación
├── basic-connection.ts # Ejemplo básico de conexión
└── [futuros ejemplos] # Ejemplos adicionales
```
## Próximos ejemplos
Se planean agregar más ejemplos que cubran:
- 📝 Creación de registros
- ✏️ Actualización de datos
- 🗑️ Eliminación de registros
- 🔍 Búsquedas avanzadas con dominios complejos
- 📊 Trabajo con diferentes modelos de Tryton
- 🔧 Configuración avanzada del cliente
## Soporte
Si encuentras problemas con los ejemplos:
1. Verifica que hayas seguido todos los pasos de preparación
2. Revisa la sección de solución de problemas
3. Asegúrate de usar la versión más reciente del cliente
Para más información sobre la API de Tryton, consulta la [documentación oficial de Tryton](https://docs.tryton.org/).

View File

@@ -0,0 +1,172 @@
/**
* Ejemplo de conexión básica con Tryton
*
* Este ejemplo demuestra cómo:
* 1. Conectarse al servidor Tryton
* 2. Obtener información del usuario y el token de sesión
* 3. Buscar terceros (party.party) y mostrar sus nombres
*
* Instrucciones de uso:
* 1. Instala las dependencias: npm install
* 2. Compila el proyecto: npm run build
* 3. Edita las variables de configuración abajo con tus datos de conexión
* 4. Ejecuta: node dist/examples/basic-connection.js
*/
import { TrytonClient } from "../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
},
};
// 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]);
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;
}
async function main() {
console.log("🚀 Iniciando ejemplo de conexión con Tryton...\n");
// Validar configuración
validateConfig();
// 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();
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.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);
}
export { main };

170
index.ts Normal file
View File

@@ -0,0 +1,170 @@
/**
* Tryton RPC Client for Node.js - TypeScript Entry Point
* Main exports for the Tryton RPC Client library
*/
// Import types and classes for internal use
import { TrytonClient } from "./client";
import type {
TrytonClientConfig,
TrytonClientOptions,
TrytonRecord,
TrytonUser,
TrytonError,
} from "./types";
// Main client class
export { TrytonClient } from "./client";
export { default as TrytonClientDefault } from "./client";
// RPC and transport classes (Node.js only)
export {
ServerProxy,
ServerPool,
Transport,
TrytonJSONEncoder,
ResponseError,
Fault,
ProtocolError,
} from "./jsonrpc";
// Cache classes
export {
TrytonCache,
CacheDict,
type CacheEntry,
type CacheStats,
type CacheFactory,
} from "./cache";
// All type definitions
export type {
// Configuration types
TrytonClientConfig,
TrytonClientOptions,
ServerProxyOptions,
ServerPoolOptions,
// Data types
TrytonRecord,
TrytonUser,
TrytonCompany,
TrytonParty,
// Search and domain types
SearchDomain,
DomainClause,
DomainOperator,
DomainLogicalOperator,
// RPC types
TrytonMethodCall,
TrytonBatchCall,
// Authentication types
LoginParameters,
LoginResult,
UserPreferences,
// Database types
DatabaseInfo,
// Error types
RpcError,
// Context types
TrytonContext,
// CRUD operation types
CreateOptions,
ReadOptions,
WriteOptions,
DeleteOptions,
SearchOptions,
SearchReadOptions,
// Client state types
ClientConfig,
// Utility types
CacheEntry as CacheEntryType,
ModelName,
FieldName,
RecordId,
RecordIds,
TypedModelOperations,
} from "./types";
// Re-export error class for convenience
export { TrytonError } from "./types";
// Version information
export const VERSION = "1.1.0";
// Default export is the main client
export default TrytonClient;
// Convenience factory functions
export function createClient(config: TrytonClientConfig): TrytonClient {
return new TrytonClient(config);
}
export function createTrytonClient(
hostname: string,
database: string,
username: string,
password: string,
port?: number,
language?: string
): TrytonClient {
return TrytonClient.create(
hostname,
database,
username,
password,
port,
language
);
}
// Type guard functions
export function isTrytonRecord(obj: any): obj is TrytonRecord {
return obj && typeof obj === "object" && typeof obj.id === "number";
}
export function isTrytonUser(obj: any): obj is TrytonUser {
return (
isTrytonRecord(obj) &&
typeof obj.login === "string" &&
typeof obj.name === "string"
);
}
export function isTrytonError(error: any): error is TrytonError {
return error instanceof Error && error.name === "TrytonError";
}
// Helper functions for React Context
export interface ReactTrytonContextConfig {
hostname?: string;
database?: string;
port?: number;
language?: string;
options?: TrytonClientOptions;
}
export function createReactTrytonConfig(
baseConfig: ReactTrytonContextConfig,
username: string,
password: string
): TrytonClientConfig {
return {
hostname: baseConfig.hostname || "localhost",
database: baseConfig.database || "tryton",
username,
password,
port: baseConfig.port || 8000,
language: baseConfig.language || "en",
options: baseConfig.options || {},
};
}

753
jsonrpc.ts Normal file
View File

@@ -0,0 +1,753 @@
/**
* JSON-RPC implementation for Tryton server communication
* Based on the Python implementation from sabatron-tryton-rpc-client
* TypeScript version - Node.js only
*/
import https from "https";
import http from "http";
import zlib from "zlib";
import { URL } from "url";
import { TrytonCache } from "./cache";
import type { ServerProxyOptions, ServerPoolOptions } from "./types";
// Constants
const CONNECT_TIMEOUT = 5000; // 5 seconds
const DEFAULT_TIMEOUT = 30000; // 30 seconds
/**
* Custom error classes
*/
export class ResponseError extends Error {
constructor(message: string) {
super(message);
this.name = "ResponseError";
}
}
export class Fault extends Error {
public readonly faultCode: string | number;
public readonly faultString: string;
public readonly extra: Record<string, any>;
constructor(
faultCode: string | number,
faultString: string = "",
extra: Record<string, any> = {}
) {
super(faultString);
this.name = "Fault";
this.faultCode = faultCode;
this.faultString = faultString;
this.extra = extra;
Object.assign(this, extra);
}
override toString(): string {
return String(this.faultCode);
}
}
export class ProtocolError extends Error {
public readonly errcode: string | number | null;
public readonly errmsg: string | null;
constructor(
message: string,
errcode: string | number | null = null,
errmsg: string | null = null
) {
super(message);
this.name = "ProtocolError";
this.errcode = errcode;
this.errmsg = errmsg;
}
}
interface TrytonDateTime {
__class__: "datetime";
year: number;
month: number;
day: number;
hour?: number;
minute?: number;
second?: number;
microsecond?: number;
}
interface TrytonDate {
__class__: "date";
year: number;
month: number;
day: number;
}
interface TrytonTime {
__class__: "time";
hour?: number;
minute?: number;
second?: number;
microsecond?: number;
}
interface TrytonTimeDelta {
__class__: "timedelta";
seconds?: number;
}
interface TrytonBytes {
__class__: "bytes";
base64: string;
}
interface TrytonDecimal {
__class__: "Decimal";
decimal: string;
}
type TrytonSpecialType =
| TrytonDateTime
| TrytonDate
| TrytonTime
| TrytonTimeDelta
| TrytonBytes
| TrytonDecimal;
/**
* JSON encoder/decoder for Tryton specific types
*/
export class TrytonJSONEncoder {
/**
* Serialize JavaScript objects to JSON with Tryton type handling
* @param {*} obj - Object to serialize
* @returns {string} - JSON string
*/
static serialize(obj: any): string {
return JSON.stringify(obj, (key: string, value: any) => {
if (value instanceof Date) {
return {
__class__: "datetime",
year: value.getFullYear(),
month: value.getMonth() + 1,
day: value.getDate(),
hour: value.getHours(),
minute: value.getMinutes(),
second: value.getSeconds(),
microsecond: value.getMilliseconds() * 1000,
} as TrytonDateTime;
}
if (typeof Buffer !== "undefined" && value instanceof Buffer) {
return {
__class__: "bytes",
base64: value.toString("base64"),
} as TrytonBytes;
}
// Handle BigInt as Decimal
if (typeof value === "bigint") {
return {
__class__: "Decimal",
decimal: value.toString(),
} as TrytonDecimal;
}
return value;
});
}
/**
* Deserialize JSON with Tryton type handling
* @param {string} str - JSON string
* @returns {*} - Parsed object
*/
static deserialize(str: string): any {
return JSON.parse(str, (key: string, value: any) => {
if (value && typeof value === "object" && value.__class__) {
const specialValue = value as TrytonSpecialType;
switch (specialValue.__class__) {
case "datetime": {
const dt = specialValue as TrytonDateTime;
return new Date(
dt.year,
dt.month - 1,
dt.day,
dt.hour || 0,
dt.minute || 0,
dt.second || 0,
Math.floor((dt.microsecond || 0) / 1000)
);
}
case "date": {
const d = specialValue as TrytonDate;
return new Date(d.year, d.month - 1, d.day);
}
case "time": {
const t = specialValue as TrytonTime;
const today = new Date();
return new Date(
today.getFullYear(),
today.getMonth(),
today.getDate(),
t.hour || 0,
t.minute || 0,
t.second || 0,
Math.floor((t.microsecond || 0) / 1000)
);
}
case "timedelta": {
const td = specialValue as TrytonTimeDelta;
// Return seconds as number
return td.seconds || 0;
}
case "bytes": {
const b = specialValue as TrytonBytes;
if (typeof Buffer !== "undefined") {
return Buffer.from(b.base64, "base64");
}
// Fallback for browser environment
return new Uint8Array(
atob(b.base64)
.split("")
.map((c) => c.charCodeAt(0))
);
}
case "Decimal": {
const dec = specialValue as TrytonDecimal;
// Convert to number or keep as string for precision
return parseFloat(dec.decimal);
}
default:
return value;
}
}
return value;
});
}
}
interface TransportOptions {
fingerprints?: string[] | null | undefined;
caCerts?: string[] | null | undefined;
session?: string | null | undefined;
connectTimeout?: number | undefined;
timeout?: number | undefined;
useHttps?: boolean;
}
interface JsonRpcRequest {
id: number;
method: string;
params: any[];
}
interface JsonRpcResponse {
id: number;
result?: any;
error?: [string | number, string];
cache?: number;
}
/**
* HTTP Transport for JSON-RPC requests
*/
export class Transport {
private fingerprints: string[] | null;
private caCerts: string[] | null;
private session: string | null;
private connection: http.ClientRequest | null;
private connectTimeout: number;
private timeout: number;
private useHttps: boolean;
constructor(options: TransportOptions = {}) {
this.fingerprints = options.fingerprints || null;
this.caCerts = options.caCerts || null;
this.session = options.session || null;
this.connection = null;
this.connectTimeout = options.connectTimeout || CONNECT_TIMEOUT;
this.timeout = options.timeout || DEFAULT_TIMEOUT;
this.useHttps = options.useHttps || false;
}
/**
* Make HTTP request to server
* @param {string} host - Server host
* @param {string} handler - URL path
* @param {string} requestData - JSON request data
* @param {boolean} verbose - Enable verbose logging
* @returns {Promise<Object>} - Response object
*/
async request(
host: string,
handler: string,
requestData: string,
verbose: boolean = false
): Promise<JsonRpcResponse> {
// Detect protocol based on port or explicit protocol
const hostParts = host.split(":");
const port = hostParts[1] ? parseInt(hostParts[1]) : 80;
const hostname = hostParts[0];
// Use HTTPS if explicitly configured, or for standard HTTPS ports
const shouldUseHttps =
this.useHttps ||
port === 443 ||
port === 8443 ||
host.startsWith("https://");
const protocol = shouldUseHttps ? "https" : "http";
const url = new URL(`${protocol}://${host}${handler}`);
const isHttps = url.protocol === "https:";
const options: https.RequestOptions = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length":
typeof Buffer !== "undefined"
? Buffer.byteLength(requestData)
: new TextEncoder().encode(requestData).length,
Connection: "keep-alive",
"Accept-Encoding": "gzip, deflate",
},
timeout: this.connectTimeout,
// Allow self-signed certificates for testing
rejectUnauthorized: false,
};
// Add session authentication
if (this.session) {
const auth =
typeof Buffer !== "undefined"
? Buffer.from(this.session).toString("base64")
: btoa(this.session);
options.headers = {
...options.headers,
Authorization: `Session ${auth}`,
};
}
return new Promise<JsonRpcResponse>((resolve, reject) => {
const client = isHttps ? https : http;
const req = client.request(options, (res: http.IncomingMessage) => {
let data =
typeof Buffer !== "undefined"
? Buffer.alloc(0)
: new Uint8Array(0);
res.on("data", (chunk: any) => {
if (typeof Buffer !== "undefined") {
data = Buffer.concat([data as Buffer, chunk]);
} else {
// Browser fallback
const newData = new Uint8Array(
(data as Uint8Array).length + chunk.length
);
newData.set(data as Uint8Array);
newData.set(chunk, (data as Uint8Array).length);
data = newData;
}
});
res.on("end", () => {
try {
// Handle compression
const encoding = res.headers["content-encoding"];
let responseText: string;
if (encoding === "gzip") {
responseText = zlib
.gunzipSync(data)
.toString("utf-8");
} else if (encoding === "deflate") {
responseText = zlib
.inflateSync(data)
.toString("utf-8");
} else {
responseText = data.toString("utf-8");
}
if (verbose) {
console.log("Response:", responseText);
}
const response = TrytonJSONEncoder.deserialize(
responseText
) as JsonRpcResponse;
// Add cache header if present
const cacheHeader = res.headers["x-tryton-cache"];
if (cacheHeader && typeof cacheHeader === "string") {
try {
response.cache = parseInt(cacheHeader);
} catch (e) {
// Ignore invalid cache header
}
}
resolve(response);
} catch (error) {
reject(
new ResponseError(
`Failed to parse response: ${
(error as Error).message
}`
)
);
}
});
});
req.on("error", (error: any) => {
if (error.code === "ECONNRESET" || error.code === "EPIPE") {
// Retry once on connection reset
reject(
new ProtocolError(
"Connection reset",
error.code,
error.message
)
);
} else {
reject(error);
}
});
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.setTimeout(this.timeout);
req.write(requestData);
req.end();
});
}
/**
* Close transport connection
*/
close(): void {
if (this.connection) {
this.connection.destroy();
this.connection = null;
}
}
}
/**
* Server proxy for making RPC calls
*/
export class ServerProxy {
private host: string;
private port: number;
private database: string;
private verbose: boolean;
private handler: string;
private hostUrl: string;
private requestId: number;
private cache: TrytonCache | null;
private useHttps: boolean;
private transport: Transport;
constructor(
host: string,
port: number,
database: string = "",
options: ServerProxyOptions = {}
) {
this.host = host;
this.port = port;
this.database = database;
this.verbose = options.verbose || false;
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
this.hostUrl = `${host}:${port}`;
this.requestId = 0;
this.cache =
options.cache && !Array.isArray(options.cache)
? (options.cache as TrytonCache)
: null;
this.useHttps = options.useHttps || false;
this.transport = new Transport({
fingerprints: options.fingerprints,
caCerts: options.caCerts,
session: options.session,
connectTimeout: options.connectTimeout,
timeout: options.timeout,
useHttps: this.useHttps,
});
}
/**
* Make RPC request with retry logic
*/
async request<T = any>(methodName: string, params: any[]): Promise<T> {
this.requestId += 1;
const id = this.requestId;
const requestData = TrytonJSONEncoder.serialize({
id: id,
method: methodName,
params: params,
} as JsonRpcRequest);
// Check cache first
if (this.cache && this.cache.cached(methodName)) {
try {
return this.cache.get(methodName, requestData);
} catch (error) {
// Cache miss or expired, continue with request
}
}
let lastError: Error | null = null;
// Retry logic (up to 5 attempts)
for (let attempt = 0; attempt < 5; attempt++) {
try {
const response = await this.transport.request(
this.hostUrl,
this.handler,
requestData,
this.verbose
);
// Validate response
if (response.id !== id) {
throw new ResponseError(
`Invalid response id (${response.id}) expected ${id}`
);
}
// Handle RPC errors
if (response.error) {
if (this.verbose) {
console.error("RPC Error:", response);
}
throw new Fault(response.error[0], response.error[1] || "");
}
// Cache successful response
if (this.cache && response.cache) {
this.cache.set(
methodName,
requestData,
response.cache,
response.result
);
}
return response.result as T;
} catch (error) {
lastError = error as Error;
// Check if we should retry
if (error instanceof ProtocolError && error.errcode === 503) {
// Service unavailable, wait and retry
const delay = Math.min(attempt + 1, 10) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
// For connection errors, try once more
if (
attempt === 0 &&
((error as any).code === "ECONNRESET" ||
(error as any).code === "EPIPE" ||
error instanceof ProtocolError)
) {
this.transport.close();
continue;
}
// Don't retry other errors
break;
}
}
throw lastError;
}
/**
* Close server proxy
*/
close(): void {
this.transport.close();
}
/**
* Get SSL status
* @returns {boolean} - Whether connection uses SSL
*/
get ssl(): boolean {
return this.port === 443 || this.hostUrl.startsWith("https");
}
/**
* Get full URL
* @returns {string} - Full server URL
*/
get url(): string {
const scheme = this.ssl ? "https" : "http";
return `${scheme}://${this.hostUrl}${this.handler}`;
}
}
/**
* Connection pool for reusing ServerProxy instances
*/
export class ServerPool {
private host: string;
private port: number;
private database: string;
private options: ServerPoolOptions;
private keepMax: number;
private session: string | null;
private pool: ServerProxy[];
private used: Set<ServerProxy>;
private cache: TrytonCache | null;
constructor(
host: string,
port: number,
database: string,
options: ServerPoolOptions = {}
) {
this.host = host;
this.port = port;
this.database = database;
this.options = options;
this.keepMax = options.keepMax || 4;
this.session = options.session || null;
this.pool = [];
this.used = new Set<ServerProxy>();
this.cache = null;
// Initialize cache if requested
if (options.cache) {
if (Array.isArray(options.cache)) {
this.cache = new TrytonCache();
} else {
this.cache = options.cache as TrytonCache;
}
}
}
/**
* Get connection from pool or create new one
* @returns {ServerProxy} - Server proxy instance
*/
getConnection(): ServerProxy {
let conn: ServerProxy;
if (this.pool.length > 0) {
conn = this.pool.pop()!;
} else {
conn = new ServerProxy(this.host, this.port, this.database, {
...this.options,
cache: this.cache as any,
});
}
this.used.add(conn);
return conn;
}
/**
* Return connection to pool
* @param {ServerProxy} conn - Connection to return
*/
putConnection(conn: ServerProxy): void {
this.used.delete(conn);
this.pool.push(conn);
// Remove excess connections
while (this.pool.length > this.keepMax) {
const oldConn = this.pool.shift();
if (oldConn) {
oldConn.close();
}
}
}
/**
* Execute callback with a pooled connection
* @param {Function} callback - Async function to execute
* @returns {Promise<*>} - Callback result
*/
async withConnection<T>(
callback: (conn: ServerProxy) => Promise<T>
): Promise<T> {
const conn = this.getConnection();
try {
return await callback(conn);
} finally {
this.putConnection(conn);
}
}
/**
* Close all connections in pool
*/
close(): void {
// Close all pooled connections
for (const conn of this.pool) {
conn.close();
}
// Close all used connections
for (const conn of this.used) {
conn.close();
}
this.pool = [];
this.used.clear();
}
/**
* Clear cache
* @param {string} [prefix] - Optional prefix to clear
*/
clearCache(prefix?: string): void {
if (this.cache) {
this.cache.clear(prefix);
}
}
/**
* Get SSL status from any connection
* @returns {boolean|null} - SSL status or null if no connections
*/
get ssl(): boolean | null {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0]?.ssl || null;
}
return null;
}
/**
* Get URL from any connection
* @returns {string|null} - URL or null if no connections
*/
get url(): string | null {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0]?.url || null;
}
return null;
}
}

581
package-lock.json generated Normal file
View File

@@ -0,0 +1,581 @@
{
"name": "@tryton/client-node",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@tryton/client-node",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"rimraf": "^5.0.0",
"typescript": "^5.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "20.19.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
}
}
}

49
package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "@tryton/client-node",
"version": "1.0.0",
"description": "Cliente RPC TypeScript para Tryton ERP - Compatible con Node.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "commonjs",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"clean": "rimraf dist",
"prepare": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1",
"example:basic": "node dist/examples/basic-connection.js"
},
"keywords": [
"tryton",
"erp",
"rpc",
"client",
"typescript",
"nodejs",
"jsonrpc"
],
"author": "",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
},
"dependencies": {},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"rimraf": "^5.0.0"
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"repository": {
"type": "git",
"url": ""
},
"bugs": {
"url": ""
},
"homepage": ""
}

30
tsconfig.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./",
"removeComments": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"types": ["node"]
},
"include": [
"*.ts",
"examples/**/*.ts"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}

284
types.ts Normal file
View File

@@ -0,0 +1,284 @@
/**
* TypeScript type definitions for Tryton RPC Client
*/
// Forward declarations
declare class TrytonCache {
cached(prefix: string): boolean;
get(prefix: string, key: string): any;
set(prefix: string, key: string, expire: number | Date, value: any): void;
clear(prefix?: string): void;
}
// ===== CONFIGURATION TYPES =====
export interface TrytonClientConfig {
hostname: string;
database: string;
username: string;
password: string;
port?: number;
language?: string;
options?: TrytonClientOptions;
}
export interface TrytonClientOptions {
verbose?: boolean;
connectTimeout?: number;
timeout?: number;
keepMax?: number;
cache?: boolean | any[];
useHttps?: boolean;
}
// ===== SERVER PROXY TYPES =====
export interface ServerProxyOptions {
verbose?: boolean;
connectTimeout?: number;
timeout?: number;
useHttps?: boolean;
fingerprints?: string[] | null;
caCerts?: string[] | null;
session?: string | null;
cache?: TrytonCache | any[] | null;
}
export interface ServerPoolOptions extends ServerProxyOptions {
session?: string;
cache?: any[] | null;
keepMax?: number;
}
// ===== TRYTON DATA TYPES =====
export interface TrytonRecord {
id: number;
[key: string]: any;
}
export interface TrytonUser extends TrytonRecord {
id: number;
name: string;
login: string;
language: string;
company: number;
email?: string;
active?: boolean;
}
export interface TrytonCompany extends TrytonRecord {
id: number;
name: string;
code?: string;
currency?: number;
}
export interface TrytonParty extends TrytonRecord {
id: number;
name: string;
code?: string;
active?: boolean;
categories?: number[];
addresses?: number[];
}
// ===== SEARCH AND DOMAIN TYPES =====
export type DomainOperator =
| "="
| "!="
| "<"
| "<="
| ">"
| ">="
| "like"
| "ilike"
| "not like"
| "not ilike"
| "in"
| "not in"
| "child_of"
| "parent_of";
export type DomainClause = [string, DomainOperator, any];
export type DomainLogicalOperator = "AND" | "OR" | "NOT";
export type SearchDomain = (
| DomainClause
| DomainLogicalOperator
| SearchDomain
)[];
// ===== RPC METHOD CALL TYPES =====
export interface TrytonMethodCall {
method: string;
args: any[];
}
export interface TrytonBatchCall extends TrytonMethodCall {
id?: string | number;
}
// ===== AUTHENTICATION TYPES =====
export interface LoginParameters {
password: string;
[key: string]: any;
}
export interface LoginResult extends Array<any> {
0: number; // user id
1: string; // session token
[key: number]: any;
}
export interface UserPreferences {
user: number;
language: string;
timezone?: string;
company?: number;
[key: string]: any;
}
// ===== DATABASE TYPES =====
export interface DatabaseInfo {
name: string;
version?: string;
[key: string]: any;
}
// ===== ERROR TYPES =====
export class TrytonError extends Error {
public readonly code?: string | number | undefined;
public readonly type?: string | undefined;
constructor(
message: string,
code?: string | number | undefined,
type?: string | undefined
) {
super(message);
this.name = "TrytonError";
this.code = code;
this.type = type;
Object.setPrototypeOf(this, TrytonError.prototype);
}
}
export interface RpcError {
message: string;
code?: string | number;
type?: string;
args?: any[];
}
// ===== CONTEXT TYPES =====
export interface TrytonContext {
language?: string;
user?: number;
company?: number;
date?: string;
timezone?: string;
groups?: number[];
[key: string]: any;
}
// ===== CRUD OPERATION TYPES =====
export interface CreateOptions {
context?: TrytonContext;
}
export interface ReadOptions {
context?: TrytonContext;
}
export interface WriteOptions {
context?: TrytonContext;
}
export interface DeleteOptions {
context?: TrytonContext;
}
export interface SearchOptions {
offset?: number;
limit?: number | null;
order?: string[] | null;
context?: TrytonContext;
}
export interface SearchReadOptions extends SearchOptions {
fields: string[];
}
// ===== CLIENT STATE TYPES =====
export interface ClientConfig {
hostname: string;
database: string;
username: string;
port: number;
language: string;
isConnected: boolean;
ssl: boolean | null;
url: string | null;
}
// ===== UTILITY TYPES =====
export type Awaitable<T> = T | Promise<T>;
export interface CacheEntry {
key: string;
value: any;
timestamp: number;
}
// ===== GENERIC MODEL TYPES =====
export type ModelName = string;
export type FieldName = string;
export type RecordId = number;
export type RecordIds = number[];
// Helper type for typed model operations
export interface TypedModelOperations<T extends TrytonRecord = TrytonRecord> {
read(
ids: RecordIds,
fields: FieldName[],
context?: TrytonContext
): Promise<T[]>;
create(
records: Partial<Omit<T, "id">>[],
context?: TrytonContext
): Promise<RecordIds>;
write(
ids: RecordIds,
values: Partial<Omit<T, "id">>,
context?: TrytonContext
): Promise<void>;
delete(ids: RecordIds, context?: TrytonContext): Promise<void>;
search(
domain: SearchDomain,
offset?: number,
limit?: number,
order?: string[],
context?: TrytonContext
): Promise<RecordIds>;
searchRead(
domain: SearchDomain,
fields: FieldName[],
offset?: number,
limit?: number,
order?: string[],
context?: TrytonContext
): Promise<T[]>;
searchCount(domain: SearchDomain, context?: TrytonContext): Promise<number>;
}
// ===== EXPORT ALL TYPES =====
// Note: All types are already exported above individually