Merge pull request 'typescript-transition' (#1) from typescript-transition into main

Reviewed-on: #1
This commit is contained in:
2025-10-11 17:38:37 -05:00
15 changed files with 1831 additions and 1888 deletions

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

@@ -1,200 +0,0 @@
/**
* Advanced usage examples for Tryton RPC Client
*/
const { TrytonClient, Fault, ProtocolError } = require("../src");
async function advancedExamples() {
console.log("🔬 Advanced Tryton RPC Client Examples");
console.log("=====================================\n");
const client = new TrytonClient({
hostname: "localhost",
database: "tryton",
username: "admin",
password: "admin",
options: {
verbose: false,
cache: true,
keepMax: 6,
timeout: 45000,
},
});
try {
await client.connect();
console.log("✅ Connected to Tryton server\n");
// Example 1: Batch operations
console.log("📦 Example 1: Batch Operations");
console.log("------------------------------");
const batchData = [
{ name: "Company A", code: "COMP_A" },
{ name: "Company B", code: "COMP_B" },
{ name: "Company C", code: "COMP_C" },
];
const createdIds = await client.create("party.party", batchData);
console.log("Created parties:", createdIds);
// Read them back
const createdParties = await client.read("party.party", createdIds, [
"id",
"name",
"code",
]);
console.log("Created party details:", createdParties);
console.log();
// Example 2: Complex search with domain
console.log("🔍 Example 2: Complex Search");
console.log("---------------------------");
const complexDomain = [
"OR",
[["name", "like", "Company%"]],
[["code", "like", "COMP_%"]],
];
const foundIds = await client.search("party.party", complexDomain);
console.log("Found IDs with complex domain:", foundIds);
const foundParties = await client.searchRead(
"party.party",
complexDomain,
["id", "name", "code"],
0, // offset
10 // limit
);
console.log("Found parties:", foundParties);
console.log();
// Example 3: Update operations
console.log("✏️ Example 3: Update Operations");
console.log("------------------------------");
if (createdIds.length > 0) {
await client.write("party.party", [createdIds[0]], {
name: "Updated Company Name",
});
console.log(`Updated party ${createdIds[0]}`);
// Verify update
const updated = await client.read(
"party.party",
[createdIds[0]],
["id", "name", "code"]
);
console.log("Updated party:", updated[0]);
}
console.log();
// Example 4: Parallel operations
console.log("⚡ Example 4: Parallel Operations");
console.log("--------------------------------");
const parallelCalls = [
{ method: "model.party.party.search_count", args: [[]] },
{ method: "model.company.company.search_count", args: [[]] },
{ method: "common.version", args: [] },
];
const parallelResults = await client.callParallel(parallelCalls);
console.log("Parallel results:");
console.log("- Total parties:", parallelResults[0]);
console.log("- Total companies:", parallelResults[1]);
console.log("- Server version:", parallelResults[2]);
console.log();
// Example 5: Working with dates
console.log("📅 Example 5: Working with Dates");
console.log("-------------------------------");
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
console.log("Today:", today.toISOString());
console.log("Tomorrow:", tomorrow.toISOString());
// Note: Date handling depends on your specific Tryton models
// This is just to show how dates are serialized
console.log();
// Example 6: Error handling patterns
console.log("⚠️ Example 6: Error Handling");
console.log("---------------------------");
try {
await client.call("nonexistent.model.method", []);
} catch (error) {
if (error instanceof Fault) {
console.log("✅ Caught RPC Fault:", error.faultCode);
} else if (error instanceof ProtocolError) {
console.log("✅ Caught Protocol Error:", error.errcode);
} else {
console.log("✅ Caught Generic Error:", error.message);
}
}
try {
await client.read("party.party", [999999], ["name"]);
} catch (error) {
console.log(
"✅ Caught read error (likely record not found):",
error.message
);
}
console.log();
// Example 7: Cache usage
console.log("💾 Example 7: Cache Usage");
console.log("------------------------");
// First call - will hit server
console.time("First call");
const result1 = await client.read("party.party", [1], ["name"]);
console.timeEnd("First call");
// Second call - should be faster due to cache
console.time("Second call (cached)");
const result2 = await client.read("party.party", [1], ["name"]);
console.timeEnd("Second call (cached)");
console.log(
"Results are equal:",
JSON.stringify(result1) === JSON.stringify(result2)
);
// Clear cache
client.clearCache();
console.log("Cache cleared");
console.log();
// Cleanup: Delete created test records
console.log("🧹 Cleanup: Deleting test records");
console.log("---------------------------------");
if (createdIds.length > 0) {
try {
await client.delete("party.party", createdIds);
console.log("✅ Deleted test records:", createdIds);
} catch (error) {
console.log("⚠️ Could not delete test records:", error.message);
}
}
} catch (error) {
console.error("❌ Advanced examples failed:", error.message);
console.error(error.stack);
} finally {
client.close();
console.log("\n👋 Advanced examples completed");
}
}
if (require.main === module) {
advancedExamples().catch(console.error);
}
module.exports = { advancedExamples };

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 "../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
},
};
// 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 };

View File

@@ -1,68 +0,0 @@
/**
* Basic usage example of Tryton RPC Client
* Simple example similar to the README
*/
const { TrytonClient } = require("../src/client");
async function basicExample() {
console.log("📖 Basic Tryton RPC Client Example");
console.log("==================================\n");
// Create client
const client = new TrytonClient({
hostname: "https://demo7.6.tryton.org", // Using HTTPS demo server
database: "demo7.6",
username: "admin",
password: "admin",
port: 8000,
language: "en",
});
try {
// Connect
console.log("Connecting...");
await client.connect();
console.log("✅ Connected!\n");
// Read a party
console.log("Reading party with ID 1...");
const party = await client.read(
"party.party",
[1],
["id", "name", "code"]
);
console.log("Party:", party[0]);
console.log();
// Create a new party
console.log("Creating new party...");
const newIds = await client.create("party.party", [
{ name: "Test Party from JS" },
]);
console.log("Created party with ID:", newIds[0]);
console.log();
// Search for parties
console.log("Searching for parties...");
const searchResults = await client.searchRead(
"party.party",
[["name", "like", "Test%"]],
["id", "name"],
0, // offset
5 // limit
);
console.log("Found parties:", searchResults);
} catch (error) {
console.error("❌ Error:", error.message);
} finally {
client.close();
console.log("\n👋 Connection closed");
}
}
if (require.main === module) {
basicExample().catch(console.error);
}
module.exports = { basicExample };

View File

@@ -1,126 +0,0 @@
/**
* Test para obtener terceros (parties) de Tryton
* Muestra el nombre de cada tercero y el total de terceros
*/
const { TrytonClient } = require("../src/client");
async function testParties() {
console.log("🏢 Test de Terceros (Parties) en Tryton");
console.log("=======================================\n");
// Configuración del servidor Tryton
// Modifica estos valores según tu servidor
//
// Ejemplos de configuración:
//
// Para servidor local:
// hostname: "localhost" o "127.0.0.1"
//
// Para servidor remoto HTTP:
// hostname: "mi-servidor.com"
//
// Para servidor remoto HTTPS:
// hostname: "https://mi-servidor.com"
//
// Para servidor demo:
// hostname: "https://demo7.6.tryton.org", database: "demo7.6"
const config = {
hostname: "https://naliia.onecluster.com.co", // Sin barra final
database: "tryton", // Cambia por el nombre de tu base de datos
username: "admin", // Tu usuario de Tryton
password: "admin", // Tu contraseña
port: 8000, // Puerto del servidor (8000 para HTTP, 8443 para HTTPS)
language: "es", // Idioma (es, en, fr, etc.)
options: {
verbose: true, // Activar para ver qué está pasando
},
};
// Crear cliente
const client = new TrytonClient(config);
try {
console.log("📡 Conectando al servidor Tryton...");
await client.connect();
console.log("✅ Conexión exitosa!\n");
// 1. Obtener el total de terceros
console.log("🔢 Obteniendo cantidad total de terceros...");
const totalParties = await client.searchCount("party.party", []);
console.log(`📊 Total de terceros en el sistema: ${totalParties}\n`);
// 2. Obtener todos los terceros con información básica
console.log("👥 Obteniendo lista de todos los terceros...");
const parties = await client.searchRead(
"party.party",
[], // Sin filtros, obtener todos
["id", "name", "code"], // Campos que queremos
0, // offset
null, // sin límite para obtener todos
null, // sin orden específico
{} // contexto vacío
);
console.log(
`\n📋 Lista completa de terceros (${parties.length} encontrados):`
);
console.log("=" + "=".repeat(60));
// Mostrar cada tercero
parties.forEach((party, index) => {
const number = (index + 1).toString().padStart(3, " ");
const id = party.id.toString().padStart(4, " ");
const code = party.code ? `[${party.code}]` : "[Sin código]";
const name = party.name || "Sin nombre";
console.log(`${number}. ID:${id} ${code.padEnd(12)} ${name}`);
});
console.log("=" + "=".repeat(60));
console.log(`\n✨ Resumen:`);
console.log(` • Total de terceros: ${totalParties}`);
console.log(` • Terceros mostrados: ${parties.length}`);
console.log(
` • Terceros con nombre: ${
parties.filter((p) => p.name && p.name.trim()).length
}`
);
console.log(
` • Terceros con código: ${
parties.filter((p) => p.code && p.code.trim()).length
}`
);
// Mostrar algunos terceros destacados si los hay
const partiesWithNames = parties.filter(
(p) => p.name && p.name.trim().length > 0
);
if (partiesWithNames.length > 0) {
console.log(`\n🌟 Algunos terceros destacados:`);
partiesWithNames.slice(0, 5).forEach((party) => {
console.log(`${party.name} (ID: ${party.id})`);
});
if (partiesWithNames.length > 5) {
console.log(` • ... y ${partiesWithNames.length - 5} más`);
}
}
} catch (error) {
console.error("\n❌ Error durante la ejecución:", error.message);
if (error.stack) {
console.error("\n📍 Stack trace:");
console.error(error.stack);
}
} finally {
console.log("\n🔌 Cerrando conexión...");
client.close();
console.log("👋 ¡Adiós!");
}
}
// Ejecutar si se llama directamente
if (require.main === module) {
testParties().catch(console.error);
}
module.exports = { testParties };

View File

@@ -1,154 +0,0 @@
/**
* Test script for Tryton RPC Client
* Equivalent to the Python test_client.py
*/
const { TrytonClient } = require("../src/client");
async function main() {
console.log("🚀 Testing Tryton RPC Client for JavaScript");
console.log("============================================\n");
// Create client instance (equivalent to Python version)
const client = new TrytonClient({
hostname: "https://demo7.6.tryton.org", // Explicitly use HTTPS
database: "demo7.6",
username: "admin",
password: "admin",
port: 8000, // Keep original port but force HTTPS
language: "en",
options: {
verbose: true, // Enable logging to see requests/responses
cache: true, // Enable caching
keepMax: 4, // Maximum pooled connections
},
});
try {
console.log("📡 Connecting to Tryton server...");
await client.connect();
console.log("✅ Connected successfully!\n");
console.log("📋 Client configuration:");
console.log(JSON.stringify(client.getConfig(), null, 2));
console.log();
// Test 1: Read party record (equivalent to Python test)
console.log("🔍 Test 1: Reading party record...");
const readResult = await client.call("model.party.party.read", [
[1],
["id", "name", "code"],
{},
]);
console.log("📄 Read result:", JSON.stringify(readResult, null, 2));
console.log();
// Test 2: Create party record (equivalent to Python test)
console.log(" Test 2: Creating new party record...");
const createResult = await client.call("model.party.party.create", [
[{ name: "Desde JavaScript" }],
{},
]);
console.log("🆕 Create result (new IDs):", createResult);
console.log();
// Test 3: Using helper methods
console.log("🛠️ Test 3: Using helper methods...");
// Read using helper method
const parties = await client.read(
"party.party",
[1],
["id", "name", "code"]
);
console.log("👥 Parties (helper method):", parties);
// Search for parties
const partyIds = await client.search(
"party.party",
[["name", "like", "%"]],
0,
5
);
console.log("🔎 Found party IDs:", partyIds);
// Search and read in one call
const partyRecords = await client.searchRead(
"party.party",
[["id", "in", partyIds.slice(0, 3)]],
["id", "name", "code"]
);
console.log("📋 Party records:", partyRecords);
// Count parties
const partyCount = await client.searchCount("party.party", []);
console.log("📊 Total parties count:", partyCount);
console.log();
// Test 4: Server information
console.log(" Test 4: Server information...");
try {
const version = await client.getVersion();
console.log("🏷️ Server version:", version);
} catch (error) {
console.log("⚠️ Could not get version:", error.message);
}
try {
const databases = await client.listDatabases();
console.log("🗄️ Available databases:", databases);
} catch (error) {
console.log("⚠️ Could not list databases:", error.message);
}
console.log();
// Test 5: Error handling
console.log("❌ Test 5: Error handling...");
try {
await client.call("invalid.method.name", []);
} catch (error) {
console.log("✅ Correctly caught error:", error.message);
}
console.log();
// Test 6: Multiple calls
console.log("🔄 Test 6: Multiple calls...");
const multipleCalls = [
{ method: "model.party.party.read", args: [[1], ["name"]] },
{ method: "model.party.party.search_count", args: [[]] },
];
const multiResults = await client.callMultiple(multipleCalls);
console.log("🔢 Multiple call results:", multiResults);
console.log();
console.log("✅ All tests completed successfully!");
} catch (error) {
console.error("❌ Test failed with error:", error.message);
console.error("Stack trace:", error.stack);
} finally {
console.log("\n🔌 Closing connection...");
client.close();
console.log("👋 Goodbye!");
}
}
// Handle unhandled promise rejections
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
process.exit(1);
});
// Handle uncaught exceptions
process.on("uncaughtException", (error) => {
console.error("Uncaught Exception:", error);
process.exit(1);
});
// Run the test
if (require.main === module) {
main().catch(console.error);
}
module.exports = { main };

1347
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +1,56 @@
{
"name": "tryton-rpc-client-js",
"version": "1.0.0",
"description": "JavaScript RPC Client for Tryton ERP Server",
"main": "src/client.js",
"description": "Cliente RPC TypeScript para Tryton ERP - Compatible con Node.js",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"type": "commonjs",
"exports": {
".": {
"import": "./dist/src/index.js",
"require": "./dist/src/index.js",
"types": "./dist/src/index.d.ts"
}
},
"scripts": {
"test": "node examples/test.js",
"example": "node examples/basic.js",
"lint": "eslint src/ examples/",
"start": "node examples/test.js"
"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",
"javascript",
"typescript",
"nodejs",
"erp",
"json-rpc"
"jsonrpc"
],
"author": "Your Name",
"license": "GPL-3.0",
"repository": {
"type": "git",
"url": "https://github.com/your-username/tryton-rpc-client-js.git"
},
"bugs": {
"url": "https://github.com/your-username/tryton-rpc-client-js/issues"
},
"homepage": "https://github.com/your-username/tryton-rpc-client-js#readme",
"author": "",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"files": [
"src/",
"examples/",
"README.md",
"LICENSE",
"CHANGELOG.md"
],
"devDependencies": {
"eslint": "^8.0.0"
"node": ">=14.0.0"
},
"dependencies": {},
"optionalDependencies": {},
"peerDependencies": {}
"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": ""
}

View File

@@ -1,15 +1,22 @@
/**
* Cache system similar to Python's CacheDict from Tryton
* Implements LRU (Least Recently Used) cache using JavaScript Map
* TypeScript version
*/
class CacheDict extends Map {
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
* @param {number} cacheLen - Maximum number of items to cache
* @param {Function} defaultFactory - Factory function for missing keys
*/
constructor(cacheLen = 10, defaultFactory = null) {
constructor(
cacheLen: number = 10,
defaultFactory: CacheFactory<V> | null = null
) {
super();
this.cacheLen = cacheLen;
this.defaultFactory = defaultFactory;
@@ -17,11 +24,8 @@ class CacheDict extends Map {
/**
* Set a key-value pair and maintain LRU order
* @param {*} key - The key
* @param {*} value - The value
* @returns {CacheDict} - This instance for chaining
*/
set(key, value) {
override set(key: K, value: V): this {
// If key exists, delete it first to move to end
if (this.has(key)) {
this.delete(key);
@@ -32,20 +36,20 @@ class CacheDict extends Map {
// 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)
* @param {*} key - The key to retrieve
* @returns {*} - The value
*/
get(key) {
override get(key: K): V | undefined {
if (this.has(key)) {
const value = super.get(key);
const value = super.get(key)!;
// Move to end by re-setting
this.delete(key);
super.set(key, value);
@@ -64,14 +68,12 @@ class CacheDict extends Map {
/**
* Override has() to update LRU order on access
* @param {*} key - The key to check
* @returns {boolean} - Whether the key exists
*/
has(key) {
override has(key: K): boolean {
const exists = super.has(key);
if (exists) {
// Move to end on access
const value = super.get(key);
const value = super.get(key)!;
this.delete(key);
super.set(key, value);
}
@@ -80,54 +82,64 @@ class CacheDict extends Map {
/**
* Get current cache size
* @returns {number} - Number of items in cache
*/
get length() {
get length(): number {
return this.size;
}
/**
* Clear all items from cache
*/
clear() {
override clear(): void {
super.clear();
}
/**
* Convert cache to array for debugging
* @returns {Array} - Array of [key, value] pairs
*/
toArray() {
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
*/
class TrytonCache {
constructor(cacheLen = 1024) {
this.store = new CacheDict(cacheLen, () => new CacheDict(cacheLen));
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
* @param {string} prefix - Method prefix
* @returns {boolean} - Whether prefix exists
*/
cached(prefix) {
cached(prefix: string): boolean {
return this.store.has(prefix);
}
/**
* Set cache entry with expiration
* @param {string} prefix - Method prefix
* @param {string} key - Cache key
* @param {number|Date} expire - Expiration time
* @param {*} value - Value to cache
*/
set(prefix, key, expire, value) {
let expiration;
set(prefix: string, key: string, expire: number | Date, value: any): void {
let expiration: Date;
if (typeof expire === "number") {
// Assume seconds, convert to Date
@@ -141,7 +153,7 @@ class TrytonCache {
// Deep copy value to avoid mutations
const cachedValue = this._deepCopy(value);
this.store.get(prefix).set(key, {
this.store.get(prefix)!.set(key, {
expire: expiration,
value: cachedValue,
});
@@ -149,24 +161,20 @@ class TrytonCache {
/**
* Get cached value if not expired
* @param {string} prefix - Method prefix
* @param {string} key - Cache key
* @returns {*} - Cached value
* @throws {Error} - If key not found or expired
*/
get(prefix, key) {
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);
const prefixCache = this.store.get(prefix)!;
if (!prefixCache.has(key)) {
throw new Error("Key not found");
}
const entry = prefixCache.get(key);
const entry = prefixCache.get(key)!;
if (entry.expire < now) {
prefixCache.delete(key);
@@ -179,12 +187,11 @@ class TrytonCache {
/**
* Clear cache for a specific prefix or all
* @param {string} [prefix] - Optional prefix to clear
*/
clear(prefix = null) {
clear(prefix?: string): void {
if (prefix) {
if (this.store.has(prefix)) {
this.store.get(prefix).clear();
this.store.get(prefix)!.clear();
}
} else {
this.store.clear();
@@ -193,35 +200,33 @@ class TrytonCache {
/**
* Deep copy objects to prevent mutations
* @param {*} obj - Object to copy
* @returns {*} - Deep copied object
* @private
*/
_deepCopy(obj) {
private _deepCopy<T>(obj: T): T {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
return new Date(obj.getTime()) as T;
}
if (obj instanceof Array) {
return obj.map((item) => this._deepCopy(item));
return obj.map((item) => this._deepCopy(item)) as T;
}
if (obj instanceof Buffer) {
return Buffer.from(obj);
// 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 = {};
const copy: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = this._deepCopy(obj[key]);
if (Object.prototype.hasOwnProperty.call(obj, key)) {
copy[key] = this._deepCopy((obj as any)[key]);
}
}
return copy;
return copy as T;
}
return obj;
@@ -229,10 +234,9 @@ class TrytonCache {
/**
* Get cache statistics
* @returns {Object} - Cache statistics
*/
getStats() {
const stats = {
getStats(): CacheStats {
const stats: CacheStats = {
totalPrefixes: this.store.size,
totalEntries: 0,
prefixes: {},
@@ -246,9 +250,48 @@ class TrytonCache {
return stats;
}
}
module.exports = {
CacheDict,
TrytonCache,
};
/**
* 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;
}
}

View File

@@ -1,26 +1,53 @@
/**
* Main Tryton RPC Client
* JavaScript implementation of sabatron-tryton-rpc-client
* Tryton RPC Client for Node.js
* TypeScript implementation of sabatron-tryton-rpc-client
*/
const { ServerProxy, ServerPool } = require("./jsonrpc");
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
*/
class TrytonClient {
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
* @param {Object} config - Configuration object
* @param {string} config.hostname - Server hostname
* @param {string} config.database - Database name
* @param {string} config.username - Username
* @param {string} config.password - Password
* @param {number} [config.port=8000] - Server port
* @param {string} [config.language='en'] - Language code
* @param {Object} [config.options={}] - Additional options
*/
constructor({
constructor(config: TrytonClientConfig) {
const {
hostname,
database,
username,
@@ -28,7 +55,8 @@ class TrytonClient {
port = 8000,
language = "en",
options = {},
}) {
} = config;
// Extract protocol from hostname if present
if (hostname.startsWith("https://")) {
this.hostname = hostname.replace("https://", "");
@@ -53,22 +81,15 @@ class TrytonClient {
/**
* Alternative constructor for backward compatibility
* @param {string} hostname - Server hostname
* @param {string} database - Database name
* @param {string} username - Username
* @param {string} password - Password
* @param {number} [port=8000] - Server port
* @param {string} [language='en'] - Language code
* @returns {TrytonClient} - New client instance
*/
static create(
hostname,
database,
username,
password,
port = 8000,
language = "en"
) {
hostname: string,
database: string,
username: string,
password: string,
port: number = 8000,
language: string = "en"
): TrytonClient {
return new TrytonClient({
hostname,
database,
@@ -84,17 +105,17 @@ class TrytonClient {
* @returns {Promise<boolean>} - True if connection successful
* @throws {Error} - If connection or authentication fails
*/
async connect() {
async connect(): Promise<boolean> {
try {
// Create proxy for login
const proxy = new ServerProxy(
this.hostname,
this.port,
this.database,
{
verbose: this.options.verbose || false,
connectTimeout: this.options.connectTimeout,
timeout: this.options.timeout,
connectTimeout:
this.options.connectTimeout || CONNECT_TIMEOUT,
timeout: this.options.timeout || DEFAULT_TIMEOUT,
useHttps: this.useHttps,
}
);
@@ -104,16 +125,14 @@ class TrytonClient {
password: this.password,
};
const result = await proxy.request("common.db.login", [
const result = await proxy.request<LoginResult>("common.db.login", [
this.username,
parameters,
this.language,
]);
// Close temporary proxy
proxy.close();
// Create session string
this.session = [this.username, ...result].join(":");
// Create connection pool with session
@@ -125,8 +144,9 @@ class TrytonClient {
session: this.session,
cache: this.options.cache !== false ? [] : null, // Enable cache by default
verbose: this.options.verbose || false,
connectTimeout: this.options.connectTimeout,
timeout: this.options.timeout,
connectTimeout:
this.options.connectTimeout || CONNECT_TIMEOUT,
timeout: this.options.timeout || DEFAULT_TIMEOUT,
keepMax: this.options.keepMax || 4,
useHttps: this.useHttps,
}
@@ -134,7 +154,7 @@ class TrytonClient {
return true;
} catch (error) {
throw new Error(`Connection failed: ${error.message}`);
throw new Error(`Connection failed: ${(error as Error).message}`);
}
}
@@ -145,13 +165,13 @@ class TrytonClient {
* @returns {Promise<*>} - Method result
* @throws {Error} - If not connected or method call fails
*/
async call(methodName, args = []) {
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(methodName, args);
return conn.request<T>(methodName, args);
});
}
@@ -160,8 +180,8 @@ class TrytonClient {
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callMultiple(calls) {
const 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);
@@ -174,7 +194,7 @@ class TrytonClient {
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callParallel(calls) {
async callParallel(calls: TrytonMethodCall[]): Promise<any[]> {
const promises = calls.map((call) => this.call(call.method, call.args));
return Promise.all(promises);
}
@@ -187,8 +207,13 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async read(model, ids, fields, context = {}) {
return this.call(`model.${model}.read`, [ids, fields, context]);
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]);
}
/**
@@ -198,8 +223,15 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of created record IDs
*/
async create(model, records, context = {}) {
return this.call(`model.${model}.create`, [records, context]);
async create(
model: ModelName,
records: Record<string, any>[],
context: TrytonContext = {}
): Promise<RecordIds> {
return this.call<RecordIds>(`model.${model}.create`, [
records,
context,
]);
}
/**
@@ -210,8 +242,13 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async write(model, ids, values, context = {}) {
return this.call(`model.${model}.write`, [ids, values, context]);
async write(
model: ModelName,
ids: RecordIds,
values: Record<string, any>,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`model.${model}.write`, [ids, values, context]);
}
/**
@@ -221,8 +258,12 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async delete(model, ids, context = {}) {
return this.call(`model.${model}.delete`, [ids, context]);
async delete(
model: ModelName,
ids: RecordIds,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`model.${model}.delete`, [ids, context]);
}
/**
@@ -236,14 +277,14 @@ class TrytonClient {
* @returns {Promise<Array<number>>} - Array of record IDs
*/
async search(
model,
domain,
offset = 0,
limit = null,
order = null,
context = {}
) {
return this.call(`model.${model}.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,
@@ -263,16 +304,16 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async searchRead(
model,
domain,
fields,
offset = 0,
limit = null,
order = null,
context = {}
) {
return this.call(`model.${model}.search_read`, [
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,
@@ -289,15 +330,22 @@ class TrytonClient {
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<number>} - Number of records
*/
async searchCount(model, domain, context = {}) {
return this.call(`model.${model}.search_count`, [domain, context]);
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() {
async getDatabaseInfo(): Promise<any> {
return this.call("common.db.get_info", []);
}
@@ -305,23 +353,49 @@ class TrytonClient {
* List available databases
* @returns {Promise<Array<string>>} - Database names
*/
async listDatabases() {
return this.call("common.db.list", []);
async listDatabases(): Promise<string[]> {
return this.call<string[]>("common.db.list", []);
}
/**
* Get server version
* @returns {Promise<string>} - Server version
*/
async getVersion() {
return this.call("common.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 = null) {
clearCache(prefix?: string): void {
if (this.connection) {
this.connection.clearCache(prefix);
}
@@ -331,7 +405,7 @@ class TrytonClient {
* Get connection SSL status
* @returns {boolean|null} - SSL status or null if not connected
*/
get ssl() {
get ssl(): boolean | null {
return this.connection ? this.connection.ssl : null;
}
@@ -339,7 +413,7 @@ class TrytonClient {
* Get connection URL
* @returns {string|null} - Connection URL or null if not connected
*/
get url() {
get url(): string | null {
return this.connection ? this.connection.url : null;
}
@@ -347,7 +421,7 @@ class TrytonClient {
* Check if client is connected
* @returns {boolean} - True if connected
*/
get isConnected() {
get isConnected(): boolean {
return this.connection !== null;
}
@@ -355,14 +429,39 @@ class TrytonClient {
* Get current session
* @returns {string|null} - Session string or null if not connected
*/
getSession() {
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() {
close(): void {
if (this.connection) {
this.connection.close();
this.connection = null;
@@ -374,7 +473,7 @@ class TrytonClient {
* Create a new client instance with the same configuration
* @returns {TrytonClient} - New client instance
*/
clone() {
clone(): TrytonClient {
return new TrytonClient({
hostname: this.hostname,
database: this.database,
@@ -390,7 +489,7 @@ class TrytonClient {
* Get client configuration (without sensitive data)
* @returns {Object} - Client configuration
*/
getConfig() {
getConfig(): ClientConfig {
return {
hostname: this.hostname,
database: this.database,
@@ -402,8 +501,77 @@ class TrytonClient {
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),
};
}
}
module.exports = {
TrytonClient,
};
// Export for CommonJS compatibility
export default TrytonClient;

View File

@@ -1,37 +0,0 @@
/**
* Main module exports
* Entry point for the Tryton RPC Client package
*/
const { TrytonClient } = require("./client");
const {
ServerProxy,
ServerPool,
ResponseError,
Fault,
ProtocolError,
TrytonJSONEncoder,
} = require("./jsonrpc");
const { CacheDict, TrytonCache } = require("./cache");
module.exports = {
// Main client class
TrytonClient,
// Low-level RPC classes
ServerProxy,
ServerPool,
// Error classes
ResponseError,
Fault,
ProtocolError,
// Utility classes
TrytonJSONEncoder,
CacheDict,
TrytonCache,
// Convenience export for backward compatibility
Client: TrytonClient,
};

170
src/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 || {},
};
}

View File

@@ -1,13 +1,15 @@
/**
* JSON-RPC implementation for Tryton server communication
* Based on the Python implementation from sabatron-tryton-rpc-client
* TypeScript version - Node.js only
*/
const https = require("https");
const http = require("http");
const zlib = require("zlib");
const { URL } = require("url");
const { TrytonCache } = require("./cache");
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
@@ -16,29 +18,45 @@ const DEFAULT_TIMEOUT = 30000; // 30 seconds
/**
* Custom error classes
*/
class ResponseError extends Error {
constructor(message) {
export class ResponseError extends Error {
constructor(message: string) {
super(message);
this.name = "ResponseError";
}
}
class Fault extends Error {
constructor(faultCode, faultString = "", extra = {}) {
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);
}
toString() {
override toString(): string {
return String(this.faultCode);
}
}
class ProtocolError extends Error {
constructor(message, errcode = null, errmsg = null) {
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;
@@ -46,17 +64,66 @@ class ProtocolError extends Error {
}
}
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
*/
class TrytonJSONEncoder {
export class TrytonJSONEncoder {
/**
* Serialize JavaScript objects to JSON with Tryton type handling
* @param {*} obj - Object to serialize
* @returns {string} - JSON string
*/
static serialize(obj) {
return JSON.stringify(obj, (key, value) => {
static serialize(obj: any): string {
return JSON.stringify(obj, (key: string, value: any) => {
if (value instanceof Date) {
return {
__class__: "datetime",
@@ -67,14 +134,14 @@ class TrytonJSONEncoder {
minute: value.getMinutes(),
second: value.getSeconds(),
microsecond: value.getMilliseconds() * 1000,
};
} as TrytonDateTime;
}
if (value instanceof Buffer) {
if (typeof Buffer !== "undefined" && value instanceof Buffer) {
return {
__class__: "bytes",
base64: value.toString("base64"),
};
} as TrytonBytes;
}
// Handle BigInt as Decimal
@@ -82,7 +149,7 @@ class TrytonJSONEncoder {
return {
__class__: "Decimal",
decimal: value.toString(),
};
} as TrytonDecimal;
}
return value;
@@ -94,46 +161,67 @@ class TrytonJSONEncoder {
* @param {string} str - JSON string
* @returns {*} - Parsed object
*/
static deserialize(str) {
return JSON.parse(str, (key, value) => {
static deserialize(str: string): any {
return JSON.parse(str, (key: string, value: any) => {
if (value && typeof value === "object" && value.__class__) {
switch (value.__class__) {
case "datetime":
const specialValue = value as TrytonSpecialType;
switch (specialValue.__class__) {
case "datetime": {
const dt = specialValue as TrytonDateTime;
return new Date(
value.year,
value.month - 1,
value.day,
value.hour || 0,
value.minute || 0,
value.second || 0,
Math.floor((value.microsecond || 0) / 1000)
dt.year,
dt.month - 1,
dt.day,
dt.hour || 0,
dt.minute || 0,
dt.second || 0,
Math.floor((dt.microsecond || 0) / 1000)
);
}
case "date":
return new Date(value.year, value.month - 1, value.day);
case "date": {
const d = specialValue as TrytonDate;
return new Date(d.year, d.month - 1, d.day);
}
case "time":
case "time": {
const t = specialValue as TrytonTime;
const today = new Date();
return new Date(
today.getFullYear(),
today.getMonth(),
today.getDate(),
value.hour || 0,
value.minute || 0,
value.second || 0,
Math.floor((value.microsecond || 0) / 1000)
t.hour || 0,
t.minute || 0,
t.second || 0,
Math.floor((t.microsecond || 0) / 1000)
);
}
case "timedelta":
case "timedelta": {
const td = specialValue as TrytonTimeDelta;
// Return seconds as number
return value.seconds || 0;
return td.seconds || 0;
}
case "bytes":
return Buffer.from(value.base64, "base64");
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":
case "Decimal": {
const dec = specialValue as TrytonDecimal;
// Convert to number or keep as string for precision
return parseFloat(value.decimal);
return parseFloat(dec.decimal);
}
default:
return value;
@@ -144,11 +232,41 @@ class TrytonJSONEncoder {
}
}
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
*/
class Transport {
constructor(options = {}) {
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;
@@ -166,7 +284,12 @@ class Transport {
* @param {boolean} verbose - Enable verbose logging
* @returns {Promise<Object>} - Response object
*/
async request(host, handler, requestData, verbose = false) {
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;
@@ -183,14 +306,17 @@ class Transport {
const url = new URL(`${protocol}://${host}${handler}`);
const isHttps = url.protocol === "https:";
const options = {
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": Buffer.byteLength(requestData),
"Content-Length":
typeof Buffer !== "undefined"
? Buffer.byteLength(requestData)
: new TextEncoder().encode(requestData).length,
Connection: "keep-alive",
"Accept-Encoding": "gzip, deflate",
},
@@ -201,25 +327,44 @@ class Transport {
// Add session authentication
if (this.session) {
const auth = Buffer.from(this.session).toString("base64");
options.headers["Authorization"] = `Session ${auth}`;
const auth =
typeof Buffer !== "undefined"
? Buffer.from(this.session).toString("base64")
: btoa(this.session);
options.headers = {
...options.headers,
Authorization: `Session ${auth}`,
};
}
return new Promise((resolve, reject) => {
return new Promise<JsonRpcResponse>((resolve, reject) => {
const client = isHttps ? https : http;
const req = client.request(options, (res) => {
let data = Buffer.alloc(0);
const req = client.request(options, (res: http.IncomingMessage) => {
let data =
typeof Buffer !== "undefined"
? Buffer.alloc(0)
: new Uint8Array(0);
res.on("data", (chunk) => {
data = Buffer.concat([data, chunk]);
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;
let responseText: string;
if (encoding === "gzip") {
responseText = zlib
@@ -237,12 +382,13 @@ class Transport {
console.log("Response:", responseText);
}
const response =
TrytonJSONEncoder.deserialize(responseText);
const response = TrytonJSONEncoder.deserialize(
responseText
) as JsonRpcResponse;
// Add cache header if present
const cacheHeader = res.headers["x-tryton-cache"];
if (cacheHeader) {
if (cacheHeader && typeof cacheHeader === "string") {
try {
response.cache = parseInt(cacheHeader);
} catch (e) {
@@ -254,14 +400,16 @@ class Transport {
} catch (error) {
reject(
new ResponseError(
`Failed to parse response: ${error.message}`
`Failed to parse response: ${
(error as Error).message
}`
)
);
}
});
});
req.on("error", (error) => {
req.on("error", (error: any) => {
if (error.code === "ECONNRESET" || error.code === "EPIPE") {
// Retry once on connection reset
reject(
@@ -290,7 +438,7 @@ class Transport {
/**
* Close transport connection
*/
close() {
close(): void {
if (this.connection) {
this.connection.destroy();
this.connection = null;
@@ -301,8 +449,24 @@ class Transport {
/**
* Server proxy for making RPC calls
*/
class ServerProxy {
constructor(host, port, database = "", options = {}) {
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;
@@ -310,7 +474,10 @@ class ServerProxy {
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
this.hostUrl = `${host}:${port}`;
this.requestId = 0;
this.cache = options.cache || null;
this.cache =
options.cache && !Array.isArray(options.cache)
? (options.cache as TrytonCache)
: null;
this.useHttps = options.useHttps || false;
this.transport = new Transport({
@@ -325,11 +492,8 @@ class ServerProxy {
/**
* Make RPC request with retry logic
* @param {string} methodName - RPC method name
* @param {Array} params - Method parameters
* @returns {Promise<*>} - Method result
*/
async request(methodName, params) {
async request<T = any>(methodName: string, params: any[]): Promise<T> {
this.requestId += 1;
const id = this.requestId;
@@ -337,7 +501,7 @@ class ServerProxy {
id: id,
method: methodName,
params: params,
});
} as JsonRpcRequest);
// Check cache first
if (this.cache && this.cache.cached(methodName)) {
@@ -348,7 +512,7 @@ class ServerProxy {
}
}
let lastError = null;
let lastError: Error | null = null;
// Retry logic (up to 5 attempts)
for (let attempt = 0; attempt < 5; attempt++) {
@@ -385,9 +549,9 @@ class ServerProxy {
);
}
return response.result;
return response.result as T;
} catch (error) {
lastError = error;
lastError = error as Error;
// Check if we should retry
if (error instanceof ProtocolError && error.errcode === 503) {
@@ -400,8 +564,8 @@ class ServerProxy {
// For connection errors, try once more
if (
attempt === 0 &&
(error.code === "ECONNRESET" ||
error.code === "EPIPE" ||
((error as any).code === "ECONNRESET" ||
(error as any).code === "EPIPE" ||
error instanceof ProtocolError)
) {
this.transport.close();
@@ -419,7 +583,7 @@ class ServerProxy {
/**
* Close server proxy
*/
close() {
close(): void {
this.transport.close();
}
@@ -427,7 +591,7 @@ class ServerProxy {
* Get SSL status
* @returns {boolean} - Whether connection uses SSL
*/
get ssl() {
get ssl(): boolean {
return this.port === 443 || this.hostUrl.startsWith("https");
}
@@ -435,7 +599,7 @@ class ServerProxy {
* Get full URL
* @returns {string} - Full server URL
*/
get url() {
get url(): string {
const scheme = this.ssl ? "https" : "http";
return `${scheme}://${this.hostUrl}${this.handler}`;
}
@@ -444,8 +608,23 @@ class ServerProxy {
/**
* Connection pool for reusing ServerProxy instances
*/
class ServerPool {
constructor(host, port, database, options = {}) {
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;
@@ -454,13 +633,16 @@ class ServerPool {
this.session = options.session || null;
this.pool = [];
this.used = new Set();
this.used = new Set<ServerProxy>();
this.cache = null;
// Initialize cache if requested
if (options.cache) {
if (Array.isArray(options.cache)) {
this.cache = new TrytonCache();
this.options.cache = this.cache;
} else {
this.cache = options.cache as TrytonCache;
}
}
}
@@ -468,15 +650,15 @@ class ServerPool {
* Get connection from pool or create new one
* @returns {ServerProxy} - Server proxy instance
*/
getConnection() {
let conn;
getConnection(): ServerProxy {
let conn: ServerProxy;
if (this.pool.length > 0) {
conn = this.pool.pop();
conn = this.pool.pop()!;
} else {
conn = new ServerProxy(this.host, this.port, this.database, {
...this.options,
cache: this.cache,
cache: this.cache as any,
});
}
@@ -488,23 +670,27 @@ class ServerPool {
* Return connection to pool
* @param {ServerProxy} conn - Connection to return
*/
putConnection(conn) {
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(callback) {
async withConnection<T>(
callback: (conn: ServerProxy) => Promise<T>
): Promise<T> {
const conn = this.getConnection();
try {
return await callback(conn);
@@ -516,7 +702,7 @@ class ServerPool {
/**
* Close all connections in pool
*/
close() {
close(): void {
// Close all pooled connections
for (const conn of this.pool) {
conn.close();
@@ -535,7 +721,7 @@ class ServerPool {
* Clear cache
* @param {string} [prefix] - Optional prefix to clear
*/
clearCache(prefix = null) {
clearCache(prefix?: string): void {
if (this.cache) {
this.cache.clear(prefix);
}
@@ -545,10 +731,10 @@ class ServerPool {
* Get SSL status from any connection
* @returns {boolean|null} - SSL status or null if no connections
*/
get ssl() {
get ssl(): boolean | null {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0].ssl;
return allConns[0]?.ssl || null;
}
return null;
}
@@ -557,21 +743,11 @@ class ServerPool {
* Get URL from any connection
* @returns {string|null} - URL or null if no connections
*/
get url() {
get url(): string | null {
const allConns = [...this.pool, ...this.used];
if (allConns.length > 0) {
return allConns[0].url;
return allConns[0]?.url || null;
}
return null;
}
}
module.exports = {
ResponseError,
Fault,
ProtocolError,
TrytonJSONEncoder,
Transport,
ServerProxy,
ServerPool,
};

31
tsconfig.json Normal file
View File

@@ -0,0 +1,31 @@
{
"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",
"src/**/*.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