Merge pull request 'typescript-transition' (#1) from typescript-transition into main
Reviewed-on: #1
This commit is contained in:
140
examples/README.md
Normal file
140
examples/README.md
Normal 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/).
|
||||||
@@ -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 };
|
|
||||||
172
examples/basic-connection.ts
Normal file
172
examples/basic-connection.ts
Normal 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 };
|
||||||
@@ -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 };
|
|
||||||
@@ -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 };
|
|
||||||
154
examples/test.js
154
examples/test.js
@@ -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
1347
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -1,47 +1,56 @@
|
|||||||
{
|
{
|
||||||
"name": "tryton-rpc-client-js",
|
"name": "tryton-rpc-client-js",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "JavaScript RPC Client for Tryton ERP Server",
|
"description": "Cliente RPC TypeScript para Tryton ERP - Compatible con Node.js",
|
||||||
"main": "src/client.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": {
|
"scripts": {
|
||||||
"test": "node examples/test.js",
|
"build": "tsc",
|
||||||
"example": "node examples/basic.js",
|
"build:watch": "tsc --watch",
|
||||||
"lint": "eslint src/ examples/",
|
"clean": "rimraf dist",
|
||||||
"start": "node examples/test.js"
|
"prepare": "npm run build",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"example:basic": "node dist/examples/basic-connection.js"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"tryton",
|
"tryton",
|
||||||
|
"erp",
|
||||||
"rpc",
|
"rpc",
|
||||||
"client",
|
"client",
|
||||||
"javascript",
|
"typescript",
|
||||||
"nodejs",
|
"nodejs",
|
||||||
"erp",
|
"jsonrpc"
|
||||||
"json-rpc"
|
|
||||||
],
|
],
|
||||||
"author": "Your Name",
|
"author": "",
|
||||||
"license": "GPL-3.0",
|
"license": "MIT",
|
||||||
"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",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"src/",
|
|
||||||
"examples/",
|
|
||||||
"README.md",
|
|
||||||
"LICENSE",
|
|
||||||
"CHANGELOG.md"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^8.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"optionalDependencies": {},
|
"devDependencies": {
|
||||||
"peerDependencies": {}
|
"@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": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* Cache system similar to Python's CacheDict from Tryton
|
* Cache system similar to Python's CacheDict from Tryton
|
||||||
* Implements LRU (Least Recently Used) cache using JavaScript Map
|
* 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
|
* 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();
|
super();
|
||||||
this.cacheLen = cacheLen;
|
this.cacheLen = cacheLen;
|
||||||
this.defaultFactory = defaultFactory;
|
this.defaultFactory = defaultFactory;
|
||||||
@@ -17,11 +24,8 @@ class CacheDict extends Map {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a key-value pair and maintain LRU order
|
* 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 key exists, delete it first to move to end
|
||||||
if (this.has(key)) {
|
if (this.has(key)) {
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
@@ -32,20 +36,20 @@ class CacheDict extends Map {
|
|||||||
// Remove oldest entries if cache is full
|
// Remove oldest entries if cache is full
|
||||||
while (this.size > this.cacheLen) {
|
while (this.size > this.cacheLen) {
|
||||||
const firstKey = this.keys().next().value;
|
const firstKey = this.keys().next().value;
|
||||||
|
if (firstKey !== undefined) {
|
||||||
this.delete(firstKey);
|
this.delete(firstKey);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a value and move it to end (most recently used)
|
* 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)) {
|
if (this.has(key)) {
|
||||||
const value = super.get(key);
|
const value = super.get(key)!;
|
||||||
// Move to end by re-setting
|
// Move to end by re-setting
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
super.set(key, value);
|
super.set(key, value);
|
||||||
@@ -64,14 +68,12 @@ class CacheDict extends Map {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Override has() to update LRU order on access
|
* 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);
|
const exists = super.has(key);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
// Move to end on access
|
// Move to end on access
|
||||||
const value = super.get(key);
|
const value = super.get(key)!;
|
||||||
this.delete(key);
|
this.delete(key);
|
||||||
super.set(key, value);
|
super.set(key, value);
|
||||||
}
|
}
|
||||||
@@ -80,54 +82,64 @@ class CacheDict extends Map {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current cache size
|
* Get current cache size
|
||||||
* @returns {number} - Number of items in cache
|
|
||||||
*/
|
*/
|
||||||
get length() {
|
get length(): number {
|
||||||
return this.size;
|
return this.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all items from cache
|
* Clear all items from cache
|
||||||
*/
|
*/
|
||||||
clear() {
|
override clear(): void {
|
||||||
super.clear();
|
super.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert cache to array for debugging
|
* Convert cache to array for debugging
|
||||||
* @returns {Array} - Array of [key, value] pairs
|
|
||||||
*/
|
*/
|
||||||
toArray() {
|
toArray(): Array<[K, V]> {
|
||||||
return Array.from(this.entries());
|
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
|
* Advanced cache for Tryton RPC with expiration support
|
||||||
*/
|
*/
|
||||||
class TrytonCache {
|
export class TrytonCache {
|
||||||
constructor(cacheLen = 1024) {
|
private store: CacheDict<string, CacheDict<string, CacheEntry>>;
|
||||||
this.store = new CacheDict(cacheLen, () => new CacheDict(cacheLen));
|
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
|
* 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);
|
return this.store.has(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set cache entry with expiration
|
* 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) {
|
set(prefix: string, key: string, expire: number | Date, value: any): void {
|
||||||
let expiration;
|
let expiration: Date;
|
||||||
|
|
||||||
if (typeof expire === "number") {
|
if (typeof expire === "number") {
|
||||||
// Assume seconds, convert to Date
|
// Assume seconds, convert to Date
|
||||||
@@ -141,7 +153,7 @@ class TrytonCache {
|
|||||||
// Deep copy value to avoid mutations
|
// Deep copy value to avoid mutations
|
||||||
const cachedValue = this._deepCopy(value);
|
const cachedValue = this._deepCopy(value);
|
||||||
|
|
||||||
this.store.get(prefix).set(key, {
|
this.store.get(prefix)!.set(key, {
|
||||||
expire: expiration,
|
expire: expiration,
|
||||||
value: cachedValue,
|
value: cachedValue,
|
||||||
});
|
});
|
||||||
@@ -149,24 +161,20 @@ class TrytonCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cached value if not expired
|
* 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();
|
const now = new Date();
|
||||||
|
|
||||||
if (!this.store.has(prefix)) {
|
if (!this.store.has(prefix)) {
|
||||||
throw new Error("Key not found");
|
throw new Error("Key not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const prefixCache = this.store.get(prefix);
|
const prefixCache = this.store.get(prefix)!;
|
||||||
if (!prefixCache.has(key)) {
|
if (!prefixCache.has(key)) {
|
||||||
throw new Error("Key not found");
|
throw new Error("Key not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const entry = prefixCache.get(key);
|
const entry = prefixCache.get(key)!;
|
||||||
|
|
||||||
if (entry.expire < now) {
|
if (entry.expire < now) {
|
||||||
prefixCache.delete(key);
|
prefixCache.delete(key);
|
||||||
@@ -179,12 +187,11 @@ class TrytonCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear cache for a specific prefix or all
|
* 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 (prefix) {
|
||||||
if (this.store.has(prefix)) {
|
if (this.store.has(prefix)) {
|
||||||
this.store.get(prefix).clear();
|
this.store.get(prefix)!.clear();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.store.clear();
|
this.store.clear();
|
||||||
@@ -193,35 +200,33 @@ class TrytonCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deep copy objects to prevent mutations
|
* 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") {
|
if (obj === null || typeof obj !== "object") {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj instanceof Date) {
|
if (obj instanceof Date) {
|
||||||
return new Date(obj.getTime());
|
return new Date(obj.getTime()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj instanceof Array) {
|
if (obj instanceof Array) {
|
||||||
return obj.map((item) => this._deepCopy(item));
|
return obj.map((item) => this._deepCopy(item)) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj instanceof Buffer) {
|
// Handle Buffer in Node.js environment
|
||||||
return Buffer.from(obj);
|
if (typeof Buffer !== "undefined" && obj instanceof Buffer) {
|
||||||
|
return Buffer.from(obj) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj === "object") {
|
if (typeof obj === "object") {
|
||||||
const copy = {};
|
const copy: any = {};
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
if (obj.hasOwnProperty(key)) {
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
copy[key] = this._deepCopy(obj[key]);
|
copy[key] = this._deepCopy((obj as any)[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return copy;
|
return copy as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
@@ -229,10 +234,9 @@ class TrytonCache {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cache statistics
|
* Get cache statistics
|
||||||
* @returns {Object} - Cache statistics
|
|
||||||
*/
|
*/
|
||||||
getStats() {
|
getStats(): CacheStats {
|
||||||
const stats = {
|
const stats: CacheStats = {
|
||||||
totalPrefixes: this.store.size,
|
totalPrefixes: this.store.size,
|
||||||
totalEntries: 0,
|
totalEntries: 0,
|
||||||
prefixes: {},
|
prefixes: {},
|
||||||
@@ -246,9 +250,48 @@ class TrytonCache {
|
|||||||
|
|
||||||
return stats;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
keysToRemove.forEach((key) => {
|
||||||
CacheDict,
|
prefixCache.delete(key);
|
||||||
TrytonCache,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,53 @@
|
|||||||
/**
|
/**
|
||||||
* Main Tryton RPC Client
|
* Tryton RPC Client for Node.js
|
||||||
* JavaScript implementation of sabatron-tryton-rpc-client
|
* 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
|
* 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
|
* 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,
|
hostname,
|
||||||
database,
|
database,
|
||||||
username,
|
username,
|
||||||
@@ -28,7 +55,8 @@ class TrytonClient {
|
|||||||
port = 8000,
|
port = 8000,
|
||||||
language = "en",
|
language = "en",
|
||||||
options = {},
|
options = {},
|
||||||
}) {
|
} = config;
|
||||||
|
|
||||||
// Extract protocol from hostname if present
|
// Extract protocol from hostname if present
|
||||||
if (hostname.startsWith("https://")) {
|
if (hostname.startsWith("https://")) {
|
||||||
this.hostname = hostname.replace("https://", "");
|
this.hostname = hostname.replace("https://", "");
|
||||||
@@ -53,22 +81,15 @@ class TrytonClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Alternative constructor for backward compatibility
|
* 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(
|
static create(
|
||||||
hostname,
|
hostname: string,
|
||||||
database,
|
database: string,
|
||||||
username,
|
username: string,
|
||||||
password,
|
password: string,
|
||||||
port = 8000,
|
port: number = 8000,
|
||||||
language = "en"
|
language: string = "en"
|
||||||
) {
|
): TrytonClient {
|
||||||
return new TrytonClient({
|
return new TrytonClient({
|
||||||
hostname,
|
hostname,
|
||||||
database,
|
database,
|
||||||
@@ -84,17 +105,17 @@ class TrytonClient {
|
|||||||
* @returns {Promise<boolean>} - True if connection successful
|
* @returns {Promise<boolean>} - True if connection successful
|
||||||
* @throws {Error} - If connection or authentication fails
|
* @throws {Error} - If connection or authentication fails
|
||||||
*/
|
*/
|
||||||
async connect() {
|
async connect(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Create proxy for login
|
|
||||||
const proxy = new ServerProxy(
|
const proxy = new ServerProxy(
|
||||||
this.hostname,
|
this.hostname,
|
||||||
this.port,
|
this.port,
|
||||||
this.database,
|
this.database,
|
||||||
{
|
{
|
||||||
verbose: this.options.verbose || false,
|
verbose: this.options.verbose || false,
|
||||||
connectTimeout: this.options.connectTimeout,
|
connectTimeout:
|
||||||
timeout: this.options.timeout,
|
this.options.connectTimeout || CONNECT_TIMEOUT,
|
||||||
|
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
||||||
useHttps: this.useHttps,
|
useHttps: this.useHttps,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -104,16 +125,14 @@ class TrytonClient {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await proxy.request("common.db.login", [
|
const result = await proxy.request<LoginResult>("common.db.login", [
|
||||||
this.username,
|
this.username,
|
||||||
parameters,
|
parameters,
|
||||||
this.language,
|
this.language,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Close temporary proxy
|
|
||||||
proxy.close();
|
proxy.close();
|
||||||
|
|
||||||
// Create session string
|
|
||||||
this.session = [this.username, ...result].join(":");
|
this.session = [this.username, ...result].join(":");
|
||||||
|
|
||||||
// Create connection pool with session
|
// Create connection pool with session
|
||||||
@@ -125,8 +144,9 @@ class TrytonClient {
|
|||||||
session: this.session,
|
session: this.session,
|
||||||
cache: this.options.cache !== false ? [] : null, // Enable cache by default
|
cache: this.options.cache !== false ? [] : null, // Enable cache by default
|
||||||
verbose: this.options.verbose || false,
|
verbose: this.options.verbose || false,
|
||||||
connectTimeout: this.options.connectTimeout,
|
connectTimeout:
|
||||||
timeout: this.options.timeout,
|
this.options.connectTimeout || CONNECT_TIMEOUT,
|
||||||
|
timeout: this.options.timeout || DEFAULT_TIMEOUT,
|
||||||
keepMax: this.options.keepMax || 4,
|
keepMax: this.options.keepMax || 4,
|
||||||
useHttps: this.useHttps,
|
useHttps: this.useHttps,
|
||||||
}
|
}
|
||||||
@@ -134,7 +154,7 @@ class TrytonClient {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} 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
|
* @returns {Promise<*>} - Method result
|
||||||
* @throws {Error} - If not connected or method call fails
|
* @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) {
|
if (!this.connection) {
|
||||||
throw new Error("Not connected. Call connect() first.");
|
throw new Error("Not connected. Call connect() first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.connection.withConnection(async (conn) => {
|
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
|
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
||||||
* @returns {Promise<Array>} - Array of results
|
* @returns {Promise<Array>} - Array of results
|
||||||
*/
|
*/
|
||||||
async callMultiple(calls) {
|
async callMultiple(calls: TrytonMethodCall[]): Promise<any[]> {
|
||||||
const results = [];
|
const results: any[] = [];
|
||||||
for (const call of calls) {
|
for (const call of calls) {
|
||||||
const result = await this.call(call.method, call.args);
|
const result = await this.call(call.method, call.args);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
@@ -174,7 +194,7 @@ class TrytonClient {
|
|||||||
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
|
||||||
* @returns {Promise<Array>} - Array of results
|
* @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));
|
const promises = calls.map((call) => this.call(call.method, call.args));
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
@@ -187,8 +207,13 @@ class TrytonClient {
|
|||||||
* @param {Object} [context={}] - Context dictionary
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<Array>} - Array of records
|
* @returns {Promise<Array>} - Array of records
|
||||||
*/
|
*/
|
||||||
async read(model, ids, fields, context = {}) {
|
async read<T extends TrytonRecord = TrytonRecord>(
|
||||||
return this.call(`model.${model}.read`, [ids, fields, context]);
|
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
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<Array<number>>} - Array of created record IDs
|
* @returns {Promise<Array<number>>} - Array of created record IDs
|
||||||
*/
|
*/
|
||||||
async create(model, records, context = {}) {
|
async create(
|
||||||
return this.call(`model.${model}.create`, [records, context]);
|
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
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async write(model, ids, values, context = {}) {
|
async write(
|
||||||
return this.call(`model.${model}.write`, [ids, values, context]);
|
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
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async delete(model, ids, context = {}) {
|
async delete(
|
||||||
return this.call(`model.${model}.delete`, [ids, context]);
|
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
|
* @returns {Promise<Array<number>>} - Array of record IDs
|
||||||
*/
|
*/
|
||||||
async search(
|
async search(
|
||||||
model,
|
model: ModelName,
|
||||||
domain,
|
domain: SearchDomain,
|
||||||
offset = 0,
|
offset: number = 0,
|
||||||
limit = null,
|
limit: number | null = null,
|
||||||
order = null,
|
order: string[] | null = null,
|
||||||
context = {}
|
context: TrytonContext = {}
|
||||||
) {
|
): Promise<RecordIds> {
|
||||||
return this.call(`model.${model}.search`, [
|
return this.call<RecordIds>(`model.${model}.search`, [
|
||||||
domain,
|
domain,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
@@ -263,16 +304,16 @@ class TrytonClient {
|
|||||||
* @param {Object} [context={}] - Context dictionary
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<Array>} - Array of records
|
* @returns {Promise<Array>} - Array of records
|
||||||
*/
|
*/
|
||||||
async searchRead(
|
async searchRead<T extends TrytonRecord = TrytonRecord>(
|
||||||
model,
|
model: ModelName,
|
||||||
domain,
|
domain: SearchDomain,
|
||||||
fields,
|
fields: FieldName[],
|
||||||
offset = 0,
|
offset: number = 0,
|
||||||
limit = null,
|
limit: number | null = null,
|
||||||
order = null,
|
order: string[] | null = null,
|
||||||
context = {}
|
context: TrytonContext = {}
|
||||||
) {
|
): Promise<T[]> {
|
||||||
return this.call(`model.${model}.search_read`, [
|
return this.call<T[]>(`model.${model}.search_read`, [
|
||||||
domain,
|
domain,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
@@ -289,15 +330,22 @@ class TrytonClient {
|
|||||||
* @param {Object} [context={}] - Context dictionary
|
* @param {Object} [context={}] - Context dictionary
|
||||||
* @returns {Promise<number>} - Number of records
|
* @returns {Promise<number>} - Number of records
|
||||||
*/
|
*/
|
||||||
async searchCount(model, domain, context = {}) {
|
async searchCount(
|
||||||
return this.call(`model.${model}.search_count`, [domain, context]);
|
model: ModelName,
|
||||||
|
domain: SearchDomain,
|
||||||
|
context: TrytonContext = {}
|
||||||
|
): Promise<number> {
|
||||||
|
return this.call<number>(`model.${model}.search_count`, [
|
||||||
|
domain,
|
||||||
|
context,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get database information
|
* Get database information
|
||||||
* @returns {Promise<Object>} - Database info
|
* @returns {Promise<Object>} - Database info
|
||||||
*/
|
*/
|
||||||
async getDatabaseInfo() {
|
async getDatabaseInfo(): Promise<any> {
|
||||||
return this.call("common.db.get_info", []);
|
return this.call("common.db.get_info", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,23 +353,49 @@ class TrytonClient {
|
|||||||
* List available databases
|
* List available databases
|
||||||
* @returns {Promise<Array<string>>} - Database names
|
* @returns {Promise<Array<string>>} - Database names
|
||||||
*/
|
*/
|
||||||
async listDatabases() {
|
async listDatabases(): Promise<string[]> {
|
||||||
return this.call("common.db.list", []);
|
return this.call<string[]>("common.db.list", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get server version
|
* Get server version
|
||||||
* @returns {Promise<string>} - Server version
|
* @returns {Promise<string>} - Server version
|
||||||
*/
|
*/
|
||||||
async getVersion() {
|
async getVersion(): Promise<string> {
|
||||||
return this.call("common.version", []);
|
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
|
* Clear cache for specific prefix or all
|
||||||
* @param {string} [prefix] - Optional prefix to clear
|
* @param {string} [prefix] - Optional prefix to clear
|
||||||
*/
|
*/
|
||||||
clearCache(prefix = null) {
|
clearCache(prefix?: string): void {
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
this.connection.clearCache(prefix);
|
this.connection.clearCache(prefix);
|
||||||
}
|
}
|
||||||
@@ -331,7 +405,7 @@ class TrytonClient {
|
|||||||
* Get connection SSL status
|
* Get connection SSL status
|
||||||
* @returns {boolean|null} - SSL status or null if not connected
|
* @returns {boolean|null} - SSL status or null if not connected
|
||||||
*/
|
*/
|
||||||
get ssl() {
|
get ssl(): boolean | null {
|
||||||
return this.connection ? this.connection.ssl : null;
|
return this.connection ? this.connection.ssl : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +413,7 @@ class TrytonClient {
|
|||||||
* Get connection URL
|
* Get connection URL
|
||||||
* @returns {string|null} - Connection URL or null if not connected
|
* @returns {string|null} - Connection URL or null if not connected
|
||||||
*/
|
*/
|
||||||
get url() {
|
get url(): string | null {
|
||||||
return this.connection ? this.connection.url : null;
|
return this.connection ? this.connection.url : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +421,7 @@ class TrytonClient {
|
|||||||
* Check if client is connected
|
* Check if client is connected
|
||||||
* @returns {boolean} - True if connected
|
* @returns {boolean} - True if connected
|
||||||
*/
|
*/
|
||||||
get isConnected() {
|
get isConnected(): boolean {
|
||||||
return this.connection !== null;
|
return this.connection !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,14 +429,39 @@ class TrytonClient {
|
|||||||
* Get current session
|
* Get current session
|
||||||
* @returns {string|null} - Session string or null if not connected
|
* @returns {string|null} - Session string or null if not connected
|
||||||
*/
|
*/
|
||||||
getSession() {
|
getSession(): string | null {
|
||||||
return this.session;
|
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 connection and cleanup resources
|
||||||
*/
|
*/
|
||||||
close() {
|
close(): void {
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
this.connection.close();
|
this.connection.close();
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
@@ -374,7 +473,7 @@ class TrytonClient {
|
|||||||
* Create a new client instance with the same configuration
|
* Create a new client instance with the same configuration
|
||||||
* @returns {TrytonClient} - New client instance
|
* @returns {TrytonClient} - New client instance
|
||||||
*/
|
*/
|
||||||
clone() {
|
clone(): TrytonClient {
|
||||||
return new TrytonClient({
|
return new TrytonClient({
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
@@ -390,7 +489,7 @@ class TrytonClient {
|
|||||||
* Get client configuration (without sensitive data)
|
* Get client configuration (without sensitive data)
|
||||||
* @returns {Object} - Client configuration
|
* @returns {Object} - Client configuration
|
||||||
*/
|
*/
|
||||||
getConfig() {
|
getConfig(): ClientConfig {
|
||||||
return {
|
return {
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
database: this.database,
|
database: this.database,
|
||||||
@@ -402,8 +501,77 @@ class TrytonClient {
|
|||||||
url: this.url,
|
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 = {
|
// Export for CommonJS compatibility
|
||||||
TrytonClient,
|
export default TrytonClient;
|
||||||
};
|
|
||||||
37
src/index.js
37
src/index.js
@@ -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
170
src/index.ts
Normal 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 || {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* JSON-RPC implementation for Tryton server communication
|
* JSON-RPC implementation for Tryton server communication
|
||||||
* Based on the Python implementation from sabatron-tryton-rpc-client
|
* Based on the Python implementation from sabatron-tryton-rpc-client
|
||||||
|
* TypeScript version - Node.js only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const https = require("https");
|
import https from "https";
|
||||||
const http = require("http");
|
import http from "http";
|
||||||
const zlib = require("zlib");
|
import zlib from "zlib";
|
||||||
const { URL } = require("url");
|
import { URL } from "url";
|
||||||
const { TrytonCache } = require("./cache");
|
import { TrytonCache } from "./cache";
|
||||||
|
import type { ServerProxyOptions, ServerPoolOptions } from "../types";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const CONNECT_TIMEOUT = 5000; // 5 seconds
|
const CONNECT_TIMEOUT = 5000; // 5 seconds
|
||||||
@@ -16,29 +18,45 @@ const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|||||||
/**
|
/**
|
||||||
* Custom error classes
|
* Custom error classes
|
||||||
*/
|
*/
|
||||||
class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
constructor(message) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ResponseError";
|
this.name = "ResponseError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Fault extends Error {
|
export class Fault extends Error {
|
||||||
constructor(faultCode, faultString = "", extra = {}) {
|
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);
|
super(faultString);
|
||||||
this.name = "Fault";
|
this.name = "Fault";
|
||||||
this.faultCode = faultCode;
|
this.faultCode = faultCode;
|
||||||
this.faultString = faultString;
|
this.faultString = faultString;
|
||||||
|
this.extra = extra;
|
||||||
Object.assign(this, extra);
|
Object.assign(this, extra);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
override toString(): string {
|
||||||
return String(this.faultCode);
|
return String(this.faultCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProtocolError extends Error {
|
export class ProtocolError extends Error {
|
||||||
constructor(message, errcode = null, errmsg = null) {
|
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);
|
super(message);
|
||||||
this.name = "ProtocolError";
|
this.name = "ProtocolError";
|
||||||
this.errcode = errcode;
|
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
|
* JSON encoder/decoder for Tryton specific types
|
||||||
*/
|
*/
|
||||||
class TrytonJSONEncoder {
|
export class TrytonJSONEncoder {
|
||||||
/**
|
/**
|
||||||
* Serialize JavaScript objects to JSON with Tryton type handling
|
* Serialize JavaScript objects to JSON with Tryton type handling
|
||||||
* @param {*} obj - Object to serialize
|
* @param {*} obj - Object to serialize
|
||||||
* @returns {string} - JSON string
|
* @returns {string} - JSON string
|
||||||
*/
|
*/
|
||||||
static serialize(obj) {
|
static serialize(obj: any): string {
|
||||||
return JSON.stringify(obj, (key, value) => {
|
return JSON.stringify(obj, (key: string, value: any) => {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return {
|
return {
|
||||||
__class__: "datetime",
|
__class__: "datetime",
|
||||||
@@ -67,14 +134,14 @@ class TrytonJSONEncoder {
|
|||||||
minute: value.getMinutes(),
|
minute: value.getMinutes(),
|
||||||
second: value.getSeconds(),
|
second: value.getSeconds(),
|
||||||
microsecond: value.getMilliseconds() * 1000,
|
microsecond: value.getMilliseconds() * 1000,
|
||||||
};
|
} as TrytonDateTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Buffer) {
|
if (typeof Buffer !== "undefined" && value instanceof Buffer) {
|
||||||
return {
|
return {
|
||||||
__class__: "bytes",
|
__class__: "bytes",
|
||||||
base64: value.toString("base64"),
|
base64: value.toString("base64"),
|
||||||
};
|
} as TrytonBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle BigInt as Decimal
|
// Handle BigInt as Decimal
|
||||||
@@ -82,7 +149,7 @@ class TrytonJSONEncoder {
|
|||||||
return {
|
return {
|
||||||
__class__: "Decimal",
|
__class__: "Decimal",
|
||||||
decimal: value.toString(),
|
decimal: value.toString(),
|
||||||
};
|
} as TrytonDecimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@@ -94,46 +161,67 @@ class TrytonJSONEncoder {
|
|||||||
* @param {string} str - JSON string
|
* @param {string} str - JSON string
|
||||||
* @returns {*} - Parsed object
|
* @returns {*} - Parsed object
|
||||||
*/
|
*/
|
||||||
static deserialize(str) {
|
static deserialize(str: string): any {
|
||||||
return JSON.parse(str, (key, value) => {
|
return JSON.parse(str, (key: string, value: any) => {
|
||||||
if (value && typeof value === "object" && value.__class__) {
|
if (value && typeof value === "object" && value.__class__) {
|
||||||
switch (value.__class__) {
|
const specialValue = value as TrytonSpecialType;
|
||||||
case "datetime":
|
switch (specialValue.__class__) {
|
||||||
|
case "datetime": {
|
||||||
|
const dt = specialValue as TrytonDateTime;
|
||||||
return new Date(
|
return new Date(
|
||||||
value.year,
|
dt.year,
|
||||||
value.month - 1,
|
dt.month - 1,
|
||||||
value.day,
|
dt.day,
|
||||||
value.hour || 0,
|
dt.hour || 0,
|
||||||
value.minute || 0,
|
dt.minute || 0,
|
||||||
value.second || 0,
|
dt.second || 0,
|
||||||
Math.floor((value.microsecond || 0) / 1000)
|
Math.floor((dt.microsecond || 0) / 1000)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "date":
|
case "date": {
|
||||||
return new Date(value.year, value.month - 1, value.day);
|
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();
|
const today = new Date();
|
||||||
return new Date(
|
return new Date(
|
||||||
today.getFullYear(),
|
today.getFullYear(),
|
||||||
today.getMonth(),
|
today.getMonth(),
|
||||||
today.getDate(),
|
today.getDate(),
|
||||||
value.hour || 0,
|
t.hour || 0,
|
||||||
value.minute || 0,
|
t.minute || 0,
|
||||||
value.second || 0,
|
t.second || 0,
|
||||||
Math.floor((value.microsecond || 0) / 1000)
|
Math.floor((t.microsecond || 0) / 1000)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
case "timedelta":
|
case "timedelta": {
|
||||||
|
const td = specialValue as TrytonTimeDelta;
|
||||||
// Return seconds as number
|
// Return seconds as number
|
||||||
return value.seconds || 0;
|
return td.seconds || 0;
|
||||||
|
}
|
||||||
|
|
||||||
case "bytes":
|
case "bytes": {
|
||||||
return Buffer.from(value.base64, "base64");
|
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
|
// Convert to number or keep as string for precision
|
||||||
return parseFloat(value.decimal);
|
return parseFloat(dec.decimal);
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return value;
|
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
|
* HTTP Transport for JSON-RPC requests
|
||||||
*/
|
*/
|
||||||
class Transport {
|
export class Transport {
|
||||||
constructor(options = {}) {
|
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.fingerprints = options.fingerprints || null;
|
||||||
this.caCerts = options.caCerts || null;
|
this.caCerts = options.caCerts || null;
|
||||||
this.session = options.session || null;
|
this.session = options.session || null;
|
||||||
@@ -166,7 +284,12 @@ class Transport {
|
|||||||
* @param {boolean} verbose - Enable verbose logging
|
* @param {boolean} verbose - Enable verbose logging
|
||||||
* @returns {Promise<Object>} - Response object
|
* @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
|
// Detect protocol based on port or explicit protocol
|
||||||
const hostParts = host.split(":");
|
const hostParts = host.split(":");
|
||||||
const port = hostParts[1] ? parseInt(hostParts[1]) : 80;
|
const port = hostParts[1] ? parseInt(hostParts[1]) : 80;
|
||||||
@@ -183,14 +306,17 @@ class Transport {
|
|||||||
const url = new URL(`${protocol}://${host}${handler}`);
|
const url = new URL(`${protocol}://${host}${handler}`);
|
||||||
const isHttps = url.protocol === "https:";
|
const isHttps = url.protocol === "https:";
|
||||||
|
|
||||||
const options = {
|
const options: https.RequestOptions = {
|
||||||
hostname: url.hostname,
|
hostname: url.hostname,
|
||||||
port: url.port || (isHttps ? 443 : 80),
|
port: url.port || (isHttps ? 443 : 80),
|
||||||
path: url.pathname + url.search,
|
path: url.pathname + url.search,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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",
|
Connection: "keep-alive",
|
||||||
"Accept-Encoding": "gzip, deflate",
|
"Accept-Encoding": "gzip, deflate",
|
||||||
},
|
},
|
||||||
@@ -201,25 +327,44 @@ class Transport {
|
|||||||
|
|
||||||
// Add session authentication
|
// Add session authentication
|
||||||
if (this.session) {
|
if (this.session) {
|
||||||
const auth = Buffer.from(this.session).toString("base64");
|
const auth =
|
||||||
options.headers["Authorization"] = `Session ${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 client = isHttps ? https : http;
|
||||||
|
|
||||||
const req = client.request(options, (res) => {
|
const req = client.request(options, (res: http.IncomingMessage) => {
|
||||||
let data = Buffer.alloc(0);
|
let data =
|
||||||
|
typeof Buffer !== "undefined"
|
||||||
|
? Buffer.alloc(0)
|
||||||
|
: new Uint8Array(0);
|
||||||
|
|
||||||
res.on("data", (chunk) => {
|
res.on("data", (chunk: any) => {
|
||||||
data = Buffer.concat([data, chunk]);
|
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", () => {
|
res.on("end", () => {
|
||||||
try {
|
try {
|
||||||
// Handle compression
|
// Handle compression
|
||||||
const encoding = res.headers["content-encoding"];
|
const encoding = res.headers["content-encoding"];
|
||||||
let responseText;
|
let responseText: string;
|
||||||
|
|
||||||
if (encoding === "gzip") {
|
if (encoding === "gzip") {
|
||||||
responseText = zlib
|
responseText = zlib
|
||||||
@@ -237,12 +382,13 @@ class Transport {
|
|||||||
console.log("Response:", responseText);
|
console.log("Response:", responseText);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response =
|
const response = TrytonJSONEncoder.deserialize(
|
||||||
TrytonJSONEncoder.deserialize(responseText);
|
responseText
|
||||||
|
) as JsonRpcResponse;
|
||||||
|
|
||||||
// Add cache header if present
|
// Add cache header if present
|
||||||
const cacheHeader = res.headers["x-tryton-cache"];
|
const cacheHeader = res.headers["x-tryton-cache"];
|
||||||
if (cacheHeader) {
|
if (cacheHeader && typeof cacheHeader === "string") {
|
||||||
try {
|
try {
|
||||||
response.cache = parseInt(cacheHeader);
|
response.cache = parseInt(cacheHeader);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -254,14 +400,16 @@ class Transport {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(
|
reject(
|
||||||
new ResponseError(
|
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") {
|
if (error.code === "ECONNRESET" || error.code === "EPIPE") {
|
||||||
// Retry once on connection reset
|
// Retry once on connection reset
|
||||||
reject(
|
reject(
|
||||||
@@ -290,7 +438,7 @@ class Transport {
|
|||||||
/**
|
/**
|
||||||
* Close transport connection
|
* Close transport connection
|
||||||
*/
|
*/
|
||||||
close() {
|
close(): void {
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
this.connection.destroy();
|
this.connection.destroy();
|
||||||
this.connection = null;
|
this.connection = null;
|
||||||
@@ -301,8 +449,24 @@ class Transport {
|
|||||||
/**
|
/**
|
||||||
* Server proxy for making RPC calls
|
* Server proxy for making RPC calls
|
||||||
*/
|
*/
|
||||||
class ServerProxy {
|
export class ServerProxy {
|
||||||
constructor(host, port, database = "", options = {}) {
|
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.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.database = database;
|
this.database = database;
|
||||||
@@ -310,7 +474,10 @@ class ServerProxy {
|
|||||||
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
|
this.handler = database ? `/${encodeURIComponent(database)}/` : "/";
|
||||||
this.hostUrl = `${host}:${port}`;
|
this.hostUrl = `${host}:${port}`;
|
||||||
this.requestId = 0;
|
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.useHttps = options.useHttps || false;
|
||||||
|
|
||||||
this.transport = new Transport({
|
this.transport = new Transport({
|
||||||
@@ -325,11 +492,8 @@ class ServerProxy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Make RPC request with retry logic
|
* 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;
|
this.requestId += 1;
|
||||||
const id = this.requestId;
|
const id = this.requestId;
|
||||||
|
|
||||||
@@ -337,7 +501,7 @@ class ServerProxy {
|
|||||||
id: id,
|
id: id,
|
||||||
method: methodName,
|
method: methodName,
|
||||||
params: params,
|
params: params,
|
||||||
});
|
} as JsonRpcRequest);
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (this.cache && this.cache.cached(methodName)) {
|
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)
|
// Retry logic (up to 5 attempts)
|
||||||
for (let attempt = 0; attempt < 5; attempt++) {
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
@@ -385,9 +549,9 @@ class ServerProxy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.result;
|
return response.result as T;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error;
|
lastError = error as Error;
|
||||||
|
|
||||||
// Check if we should retry
|
// Check if we should retry
|
||||||
if (error instanceof ProtocolError && error.errcode === 503) {
|
if (error instanceof ProtocolError && error.errcode === 503) {
|
||||||
@@ -400,8 +564,8 @@ class ServerProxy {
|
|||||||
// For connection errors, try once more
|
// For connection errors, try once more
|
||||||
if (
|
if (
|
||||||
attempt === 0 &&
|
attempt === 0 &&
|
||||||
(error.code === "ECONNRESET" ||
|
((error as any).code === "ECONNRESET" ||
|
||||||
error.code === "EPIPE" ||
|
(error as any).code === "EPIPE" ||
|
||||||
error instanceof ProtocolError)
|
error instanceof ProtocolError)
|
||||||
) {
|
) {
|
||||||
this.transport.close();
|
this.transport.close();
|
||||||
@@ -419,7 +583,7 @@ class ServerProxy {
|
|||||||
/**
|
/**
|
||||||
* Close server proxy
|
* Close server proxy
|
||||||
*/
|
*/
|
||||||
close() {
|
close(): void {
|
||||||
this.transport.close();
|
this.transport.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +591,7 @@ class ServerProxy {
|
|||||||
* Get SSL status
|
* Get SSL status
|
||||||
* @returns {boolean} - Whether connection uses SSL
|
* @returns {boolean} - Whether connection uses SSL
|
||||||
*/
|
*/
|
||||||
get ssl() {
|
get ssl(): boolean {
|
||||||
return this.port === 443 || this.hostUrl.startsWith("https");
|
return this.port === 443 || this.hostUrl.startsWith("https");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +599,7 @@ class ServerProxy {
|
|||||||
* Get full URL
|
* Get full URL
|
||||||
* @returns {string} - Full server URL
|
* @returns {string} - Full server URL
|
||||||
*/
|
*/
|
||||||
get url() {
|
get url(): string {
|
||||||
const scheme = this.ssl ? "https" : "http";
|
const scheme = this.ssl ? "https" : "http";
|
||||||
return `${scheme}://${this.hostUrl}${this.handler}`;
|
return `${scheme}://${this.hostUrl}${this.handler}`;
|
||||||
}
|
}
|
||||||
@@ -444,8 +608,23 @@ class ServerProxy {
|
|||||||
/**
|
/**
|
||||||
* Connection pool for reusing ServerProxy instances
|
* Connection pool for reusing ServerProxy instances
|
||||||
*/
|
*/
|
||||||
class ServerPool {
|
export class ServerPool {
|
||||||
constructor(host, port, database, options = {}) {
|
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.host = host;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.database = database;
|
this.database = database;
|
||||||
@@ -454,13 +633,16 @@ class ServerPool {
|
|||||||
this.session = options.session || null;
|
this.session = options.session || null;
|
||||||
|
|
||||||
this.pool = [];
|
this.pool = [];
|
||||||
this.used = new Set();
|
this.used = new Set<ServerProxy>();
|
||||||
this.cache = null;
|
this.cache = null;
|
||||||
|
|
||||||
// Initialize cache if requested
|
// Initialize cache if requested
|
||||||
if (options.cache) {
|
if (options.cache) {
|
||||||
|
if (Array.isArray(options.cache)) {
|
||||||
this.cache = new TrytonCache();
|
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
|
* Get connection from pool or create new one
|
||||||
* @returns {ServerProxy} - Server proxy instance
|
* @returns {ServerProxy} - Server proxy instance
|
||||||
*/
|
*/
|
||||||
getConnection() {
|
getConnection(): ServerProxy {
|
||||||
let conn;
|
let conn: ServerProxy;
|
||||||
|
|
||||||
if (this.pool.length > 0) {
|
if (this.pool.length > 0) {
|
||||||
conn = this.pool.pop();
|
conn = this.pool.pop()!;
|
||||||
} else {
|
} else {
|
||||||
conn = new ServerProxy(this.host, this.port, this.database, {
|
conn = new ServerProxy(this.host, this.port, this.database, {
|
||||||
...this.options,
|
...this.options,
|
||||||
cache: this.cache,
|
cache: this.cache as any,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,23 +670,27 @@ class ServerPool {
|
|||||||
* Return connection to pool
|
* Return connection to pool
|
||||||
* @param {ServerProxy} conn - Connection to return
|
* @param {ServerProxy} conn - Connection to return
|
||||||
*/
|
*/
|
||||||
putConnection(conn) {
|
putConnection(conn: ServerProxy): void {
|
||||||
this.used.delete(conn);
|
this.used.delete(conn);
|
||||||
this.pool.push(conn);
|
this.pool.push(conn);
|
||||||
|
|
||||||
// Remove excess connections
|
// Remove excess connections
|
||||||
while (this.pool.length > this.keepMax) {
|
while (this.pool.length > this.keepMax) {
|
||||||
const oldConn = this.pool.shift();
|
const oldConn = this.pool.shift();
|
||||||
|
if (oldConn) {
|
||||||
oldConn.close();
|
oldConn.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute callback with a pooled connection
|
* Execute callback with a pooled connection
|
||||||
* @param {Function} callback - Async function to execute
|
* @param {Function} callback - Async function to execute
|
||||||
* @returns {Promise<*>} - Callback result
|
* @returns {Promise<*>} - Callback result
|
||||||
*/
|
*/
|
||||||
async withConnection(callback) {
|
async withConnection<T>(
|
||||||
|
callback: (conn: ServerProxy) => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
const conn = this.getConnection();
|
const conn = this.getConnection();
|
||||||
try {
|
try {
|
||||||
return await callback(conn);
|
return await callback(conn);
|
||||||
@@ -516,7 +702,7 @@ class ServerPool {
|
|||||||
/**
|
/**
|
||||||
* Close all connections in pool
|
* Close all connections in pool
|
||||||
*/
|
*/
|
||||||
close() {
|
close(): void {
|
||||||
// Close all pooled connections
|
// Close all pooled connections
|
||||||
for (const conn of this.pool) {
|
for (const conn of this.pool) {
|
||||||
conn.close();
|
conn.close();
|
||||||
@@ -535,7 +721,7 @@ class ServerPool {
|
|||||||
* Clear cache
|
* Clear cache
|
||||||
* @param {string} [prefix] - Optional prefix to clear
|
* @param {string} [prefix] - Optional prefix to clear
|
||||||
*/
|
*/
|
||||||
clearCache(prefix = null) {
|
clearCache(prefix?: string): void {
|
||||||
if (this.cache) {
|
if (this.cache) {
|
||||||
this.cache.clear(prefix);
|
this.cache.clear(prefix);
|
||||||
}
|
}
|
||||||
@@ -545,10 +731,10 @@ class ServerPool {
|
|||||||
* Get SSL status from any connection
|
* Get SSL status from any connection
|
||||||
* @returns {boolean|null} - SSL status or null if no connections
|
* @returns {boolean|null} - SSL status or null if no connections
|
||||||
*/
|
*/
|
||||||
get ssl() {
|
get ssl(): boolean | null {
|
||||||
const allConns = [...this.pool, ...this.used];
|
const allConns = [...this.pool, ...this.used];
|
||||||
if (allConns.length > 0) {
|
if (allConns.length > 0) {
|
||||||
return allConns[0].ssl;
|
return allConns[0]?.ssl || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -557,21 +743,11 @@ class ServerPool {
|
|||||||
* Get URL from any connection
|
* Get URL from any connection
|
||||||
* @returns {string|null} - URL or null if no connections
|
* @returns {string|null} - URL or null if no connections
|
||||||
*/
|
*/
|
||||||
get url() {
|
get url(): string | null {
|
||||||
const allConns = [...this.pool, ...this.used];
|
const allConns = [...this.pool, ...this.used];
|
||||||
if (allConns.length > 0) {
|
if (allConns.length > 0) {
|
||||||
return allConns[0].url;
|
return allConns[0]?.url || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
ResponseError,
|
|
||||||
Fault,
|
|
||||||
ProtocolError,
|
|
||||||
TrytonJSONEncoder,
|
|
||||||
Transport,
|
|
||||||
ServerProxy,
|
|
||||||
ServerPool,
|
|
||||||
};
|
|
||||||
31
tsconfig.json
Normal file
31
tsconfig.json
Normal 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
284
types.ts
Normal 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
|
||||||
Reference in New Issue
Block a user