From 07714664339650984ffb9f96558ca9e2842712bd Mon Sep 17 00:00:00 2001 From: Juan Diego Moreno Upegui Date: Fri, 26 Sep 2025 14:04:07 -0500 Subject: [PATCH] Add complete Tryton RPC client implementation with examples and tests --- .gitignore | 114 ++++ CHANGELOG.md | 72 +++ LICENSE | 29 + examples/advanced.js | 200 ++++++ examples/basic.js | 68 +++ examples/test-parties.js | 126 ++++ examples/test.js | 154 +++++ package-lock.json | 1246 ++++++++++++++++++++++++++++++++++++++ package.json | 47 ++ src/cache.js | 254 ++++++++ src/client.js | 409 +++++++++++++ src/index.js | 37 ++ src/jsonrpc.js | 577 ++++++++++++++++++ 13 files changed, 3333 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 examples/advanced.js create mode 100644 examples/basic.js create mode 100644 examples/test-parties.js create mode 100644 examples/test.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/cache.js create mode 100644 src/client.js create mode 100644 src/index.js create mode 100644 src/jsonrpc.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be21527 --- /dev/null +++ b/.gitignore @@ -0,0 +1,114 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Test configuration files +test-config.json +local-config.js \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3f5c16b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-09-26 + +### Added + +- Initial release of Tryton RPC Client for JavaScript +- Full JSON-RPC implementation compatible with Tryton server +- TrytonClient class with comprehensive API +- Connection pooling support with ServerPool +- Smart caching system with LRU cache and expiration +- Automatic session management and authentication +- Helper methods for CRUD operations (create, read, write, delete) +- Helper methods for search operations (search, searchRead, searchCount) +- Type-safe serialization/deserialization of Tryton data types: + - Date/DateTime objects + - Decimal numbers + - Bytes/Buffer objects + - TimeDelta values +- Comprehensive error handling: + - Fault errors for RPC issues + - ProtocolError for HTTP/connection issues + - ResponseError for invalid responses +- Retry logic for failed requests +- SSL/TLS support +- Configurable timeouts and connection limits +- Verbose logging support +- Multiple example files demonstrating usage +- Complete documentation in README.md +- MIT-compatible dependencies (none required) + +### Features + +- ✅ Connection management with automatic session handling +- ✅ Pool of reusable connections for efficiency +- ✅ Smart caching with configurable expiration +- ✅ Complete CRUD operation helpers +- ✅ Advanced search functionality +- ✅ Batch operations support +- ✅ Parallel request execution +- ✅ Comprehensive error handling +- ✅ Type safety for Tryton data types +- ✅ Server information methods +- ✅ Cache management utilities +- ✅ Connection cloning support +- ✅ Configurable client options + +### Documentation + +- Complete API documentation in README.md +- Basic usage examples +- Advanced usage patterns +- Error handling examples +- Configuration options documentation + +### Examples + +- `examples/basic.js` - Simple usage example +- `examples/test.js` - Comprehensive test suite +- `examples/advanced.js` - Advanced patterns and best practices + +### Compatibility + +- Node.js >= 12.0.0 +- Tryton Server 6.x and 7.x +- JSON-RPC protocol support +- No external dependencies required diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3995df2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for +software and other kinds of works. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We guarantee these rights by giving you +certain conditions under which you can use, copy, distribute and modify +the work. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +[Full GPL v3 license text would continue here...] diff --git a/examples/advanced.js b/examples/advanced.js new file mode 100644 index 0000000..8758c92 --- /dev/null +++ b/examples/advanced.js @@ -0,0 +1,200 @@ +/** + * 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 }; diff --git a/examples/basic.js b/examples/basic.js new file mode 100644 index 0000000..e095314 --- /dev/null +++ b/examples/basic.js @@ -0,0 +1,68 @@ +/** + * 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 }; diff --git a/examples/test-parties.js b/examples/test-parties.js new file mode 100644 index 0000000..adf9b03 --- /dev/null +++ b/examples/test-parties.js @@ -0,0 +1,126 @@ +/** + * 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 }; diff --git a/examples/test.js b/examples/test.js new file mode 100644 index 0000000..971593a --- /dev/null +++ b/examples/test.js @@ -0,0 +1,154 @@ +/** + * 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 }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e0f99bc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1246 @@ +{ + "name": "tryton-rpc-client-js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tryton-rpc-client-js", + "version": "1.0.0", + "license": "GPL-3.0", + "devDependencies": { + "eslint": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0aab5e5 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "tryton-rpc-client-js", + "version": "1.0.0", + "description": "JavaScript RPC Client for Tryton ERP Server", + "main": "src/client.js", + "scripts": { + "test": "node examples/test.js", + "example": "node examples/basic.js", + "lint": "eslint src/ examples/", + "start": "node examples/test.js" + }, + "keywords": [ + "tryton", + "rpc", + "client", + "javascript", + "nodejs", + "erp", + "json-rpc" + ], + "author": "Your Name", + "license": "GPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/your-username/tryton-rpc-client-js.git" + }, + "bugs": { + "url": "https://github.com/your-username/tryton-rpc-client-js/issues" + }, + "homepage": "https://github.com/your-username/tryton-rpc-client-js#readme", + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "src/", + "examples/", + "README.md", + "LICENSE", + "CHANGELOG.md" + ], + "devDependencies": { + "eslint": "^8.0.0" + }, + "dependencies": {}, + "optionalDependencies": {}, + "peerDependencies": {} +} diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 0000000..9b28d17 --- /dev/null +++ b/src/cache.js @@ -0,0 +1,254 @@ +/** + * Cache system similar to Python's CacheDict from Tryton + * Implements LRU (Least Recently Used) cache using JavaScript Map + */ + +class CacheDict extends Map { + /** + * 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) { + super(); + this.cacheLen = cacheLen; + this.defaultFactory = defaultFactory; + } + + /** + * 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) { + // If key exists, delete it first to move to end + if (this.has(key)) { + this.delete(key); + } + + super.set(key, value); + + // Remove oldest entries if cache is full + while (this.size > this.cacheLen) { + const firstKey = this.keys().next().value; + this.delete(firstKey); + } + + return this; + } + + /** + * Get a value and move it to end (most recently used) + * @param {*} key - The key to retrieve + * @returns {*} - The value + */ + get(key) { + if (this.has(key)) { + const value = super.get(key); + // Move to end by re-setting + this.delete(key); + super.set(key, value); + return value; + } + + // Handle missing key with default factory + if (this.defaultFactory) { + const value = this.defaultFactory(); + this.set(key, value); + return value; + } + + return undefined; + } + + /** + * Override has() to update LRU order on access + * @param {*} key - The key to check + * @returns {boolean} - Whether the key exists + */ + has(key) { + const exists = super.has(key); + if (exists) { + // Move to end on access + const value = super.get(key); + this.delete(key); + super.set(key, value); + } + return exists; + } + + /** + * Get current cache size + * @returns {number} - Number of items in cache + */ + get length() { + return this.size; + } + + /** + * Clear all items from cache + */ + clear() { + super.clear(); + } + + /** + * Convert cache to array for debugging + * @returns {Array} - Array of [key, value] pairs + */ + toArray() { + return Array.from(this.entries()); + } +} + +/** + * Advanced cache for Tryton RPC with expiration support + */ +class TrytonCache { + constructor(cacheLen = 1024) { + this.store = new CacheDict(cacheLen, () => new CacheDict(cacheLen)); + } + + /** + * Check if a prefix is cached + * @param {string} prefix - Method prefix + * @returns {boolean} - Whether prefix exists + */ + cached(prefix) { + return this.store.has(prefix); + } + + /** + * Set cache entry with expiration + * @param {string} prefix - Method prefix + * @param {string} key - Cache key + * @param {number|Date} expire - Expiration time + * @param {*} value - Value to cache + */ + set(prefix, key, expire, value) { + let expiration; + + if (typeof expire === "number") { + // Assume seconds, convert to Date + expiration = new Date(Date.now() + expire * 1000); + } else if (expire instanceof Date) { + expiration = expire; + } else { + throw new Error("Invalid expiration type"); + } + + // Deep copy value to avoid mutations + const cachedValue = this._deepCopy(value); + + this.store.get(prefix).set(key, { + expire: expiration, + value: cachedValue, + }); + } + + /** + * Get cached value if not expired + * @param {string} prefix - Method prefix + * @param {string} key - Cache key + * @returns {*} - Cached value + * @throws {Error} - If key not found or expired + */ + get(prefix, key) { + const now = new Date(); + + if (!this.store.has(prefix)) { + throw new Error("Key not found"); + } + + const prefixCache = this.store.get(prefix); + if (!prefixCache.has(key)) { + throw new Error("Key not found"); + } + + const entry = prefixCache.get(key); + + if (entry.expire < now) { + prefixCache.delete(key); + throw new Error("Key expired"); + } + + console.log(`(cached) ${prefix} ${key}`); + return this._deepCopy(entry.value); + } + + /** + * Clear cache for a specific prefix or all + * @param {string} [prefix] - Optional prefix to clear + */ + clear(prefix = null) { + if (prefix) { + if (this.store.has(prefix)) { + this.store.get(prefix).clear(); + } + } else { + this.store.clear(); + } + } + + /** + * Deep copy objects to prevent mutations + * @param {*} obj - Object to copy + * @returns {*} - Deep copied object + * @private + */ + _deepCopy(obj) { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()); + } + + if (obj instanceof Array) { + return obj.map((item) => this._deepCopy(item)); + } + + if (obj instanceof Buffer) { + return Buffer.from(obj); + } + + if (typeof obj === "object") { + const copy = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + copy[key] = this._deepCopy(obj[key]); + } + } + return copy; + } + + return obj; + } + + /** + * Get cache statistics + * @returns {Object} - Cache statistics + */ + getStats() { + const stats = { + totalPrefixes: this.store.size, + totalEntries: 0, + prefixes: {}, + }; + + for (const [prefix, prefixCache] of this.store.entries()) { + const count = prefixCache.size; + stats.totalEntries += count; + stats.prefixes[prefix] = count; + } + + return stats; + } +} + +module.exports = { + CacheDict, + TrytonCache, +}; diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..69b3417 --- /dev/null +++ b/src/client.js @@ -0,0 +1,409 @@ +/** + * Main Tryton RPC Client + * JavaScript implementation of sabatron-tryton-rpc-client + */ + +const { ServerProxy, ServerPool } = require("./jsonrpc"); + +/** + * Main client class for connecting to Tryton server via RPC + */ +class TrytonClient { + /** + * 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({ + hostname, + database, + username, + password, + port = 8000, + language = "en", + options = {}, + }) { + // Extract protocol from hostname if present + if (hostname.startsWith("https://")) { + this.hostname = hostname.replace("https://", ""); + this.useHttps = true; + } else if (hostname.startsWith("http://")) { + this.hostname = hostname.replace("http://", ""); + this.useHttps = false; + } else { + this.hostname = hostname; + this.useHttps = port === 443 || port === 8443; + } + + this.database = database; + this.username = username; + this.password = password; + this.port = port; + this.language = language; + this.options = options; + this.connection = null; + this.session = null; + } + + /** + * Alternative constructor for backward compatibility + * @param {string} hostname - Server hostname + * @param {string} database - Database name + * @param {string} username - Username + * @param {string} password - Password + * @param {number} [port=8000] - Server port + * @param {string} [language='en'] - Language code + * @returns {TrytonClient} - New client instance + */ + static create( + hostname, + database, + username, + password, + port = 8000, + language = "en" + ) { + return new TrytonClient({ + hostname, + database, + username, + password, + port, + language, + }); + } + + /** + * Connect to Tryton server and authenticate + * @returns {Promise} - True if connection successful + * @throws {Error} - If connection or authentication fails + */ + async connect() { + try { + // Create proxy for login + const proxy = new ServerProxy( + this.hostname, + this.port, + this.database, + { + verbose: this.options.verbose || false, + connectTimeout: this.options.connectTimeout, + timeout: this.options.timeout, + useHttps: this.useHttps, + } + ); + + // Perform login + const parameters = { + password: this.password, + }; + + const result = await proxy.request("common.db.login", [ + this.username, + parameters, + this.language, + ]); + + // Close temporary proxy + proxy.close(); + + // Create session string + this.session = [this.username, ...result].join(":"); + + // Create connection pool with session + this.connection = new ServerPool( + this.hostname, + this.port, + this.database, + { + session: this.session, + cache: this.options.cache !== false ? [] : null, // Enable cache by default + verbose: this.options.verbose || false, + connectTimeout: this.options.connectTimeout, + timeout: this.options.timeout, + keepMax: this.options.keepMax || 4, + useHttps: this.useHttps, + } + ); + + return true; + } catch (error) { + throw new Error(`Connection failed: ${error.message}`); + } + } + + /** + * Call RPC method on server + * @param {string} methodName - RPC method name (e.g., 'model.party.party.read') + * @param {Array} args - Method arguments + * @returns {Promise<*>} - Method result + * @throws {Error} - If not connected or method call fails + */ + async call(methodName, args = []) { + if (!this.connection) { + throw new Error("Not connected. Call connect() first."); + } + + return this.connection.withConnection(async (conn) => { + return conn.request(methodName, args); + }); + } + + /** + * Call multiple RPC methods in sequence + * @param {Array<{method: string, args: Array}>} calls - Array of method calls + * @returns {Promise} - Array of results + */ + async callMultiple(calls) { + const results = []; + for (const call of calls) { + const result = await this.call(call.method, call.args); + results.push(result); + } + return results; + } + + /** + * Call multiple RPC methods in parallel + * @param {Array<{method: string, args: Array}>} calls - Array of method calls + * @returns {Promise} - Array of results + */ + async callParallel(calls) { + const promises = calls.map((call) => this.call(call.method, call.args)); + return Promise.all(promises); + } + + /** + * Helper method to read records from a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to read + * @param {Array} fields - Fields to read + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Array of records + */ + async read(model, ids, fields, context = {}) { + return this.call(`model.${model}.read`, [ids, fields, context]); + } + + /** + * Helper method to create records in a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} records - Records to create + * @param {Object} [context={}] - Context dictionary + * @returns {Promise>} - Array of created record IDs + */ + async create(model, records, context = {}) { + return this.call(`model.${model}.create`, [records, context]); + } + + /** + * Helper method to write/update records in a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to update + * @param {Object} values - Values to update + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} + */ + async write(model, ids, values, context = {}) { + return this.call(`model.${model}.write`, [ids, values, context]); + } + + /** + * Helper method to delete records from a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to delete + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} + */ + async delete(model, ids, context = {}) { + return this.call(`model.${model}.delete`, [ids, context]); + } + + /** + * Helper method to search for records + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {number} [offset=0] - Offset for pagination + * @param {number} [limit=null] - Limit for pagination + * @param {Array} [order=null] - Order specification + * @param {Object} [context={}] - Context dictionary + * @returns {Promise>} - Array of record IDs + */ + async search( + model, + domain, + offset = 0, + limit = null, + order = null, + context = {} + ) { + return this.call(`model.${model}.search`, [ + domain, + offset, + limit, + order, + context, + ]); + } + + /** + * Helper method to search and read records in one call + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {Array} fields - Fields to read + * @param {number} [offset=0] - Offset for pagination + * @param {number} [limit=null] - Limit for pagination + * @param {Array} [order=null] - Order specification + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Array of records + */ + async searchRead( + model, + domain, + fields, + offset = 0, + limit = null, + order = null, + context = {} + ) { + return this.call(`model.${model}.search_read`, [ + domain, + offset, + limit, + order, + fields, + context, + ]); + } + + /** + * Helper method to count records + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Number of records + */ + async searchCount(model, domain, context = {}) { + return this.call(`model.${model}.search_count`, [domain, context]); + } + + /** + * Get database information + * @returns {Promise} - Database info + */ + async getDatabaseInfo() { + return this.call("common.db.get_info", []); + } + + /** + * List available databases + * @returns {Promise>} - Database names + */ + async listDatabases() { + return this.call("common.db.list", []); + } + + /** + * Get server version + * @returns {Promise} - Server version + */ + async getVersion() { + return this.call("common.version", []); + } + + /** + * Clear cache for specific prefix or all + * @param {string} [prefix] - Optional prefix to clear + */ + clearCache(prefix = null) { + if (this.connection) { + this.connection.clearCache(prefix); + } + } + + /** + * Get connection SSL status + * @returns {boolean|null} - SSL status or null if not connected + */ + get ssl() { + return this.connection ? this.connection.ssl : null; + } + + /** + * Get connection URL + * @returns {string|null} - Connection URL or null if not connected + */ + get url() { + return this.connection ? this.connection.url : null; + } + + /** + * Check if client is connected + * @returns {boolean} - True if connected + */ + get isConnected() { + return this.connection !== null; + } + + /** + * Get current session + * @returns {string|null} - Session string or null if not connected + */ + getSession() { + return this.session; + } + + /** + * Close connection and cleanup resources + */ + close() { + if (this.connection) { + this.connection.close(); + this.connection = null; + this.session = null; + } + } + + /** + * Create a new client instance with the same configuration + * @returns {TrytonClient} - New client instance + */ + clone() { + return new TrytonClient({ + hostname: this.hostname, + database: this.database, + username: this.username, + password: this.password, + port: this.port, + language: this.language, + options: { ...this.options }, + }); + } + + /** + * Get client configuration (without sensitive data) + * @returns {Object} - Client configuration + */ + getConfig() { + return { + hostname: this.hostname, + database: this.database, + username: this.username, + port: this.port, + language: this.language, + isConnected: this.isConnected, + ssl: this.ssl, + url: this.url, + }; + } +} + +module.exports = { + TrytonClient, +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..819d3fa --- /dev/null +++ b/src/index.js @@ -0,0 +1,37 @@ +/** + * 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, +}; diff --git a/src/jsonrpc.js b/src/jsonrpc.js new file mode 100644 index 0000000..7367647 --- /dev/null +++ b/src/jsonrpc.js @@ -0,0 +1,577 @@ +/** + * JSON-RPC implementation for Tryton server communication + * Based on the Python implementation from sabatron-tryton-rpc-client + */ + +const https = require("https"); +const http = require("http"); +const zlib = require("zlib"); +const { URL } = require("url"); +const { TrytonCache } = require("./cache"); + +// Constants +const CONNECT_TIMEOUT = 5000; // 5 seconds +const DEFAULT_TIMEOUT = 30000; // 30 seconds + +/** + * Custom error classes + */ +class ResponseError extends Error { + constructor(message) { + super(message); + this.name = "ResponseError"; + } +} + +class Fault extends Error { + constructor(faultCode, faultString = "", extra = {}) { + super(faultString); + this.name = "Fault"; + this.faultCode = faultCode; + this.faultString = faultString; + Object.assign(this, extra); + } + + toString() { + return String(this.faultCode); + } +} + +class ProtocolError extends Error { + constructor(message, errcode = null, errmsg = null) { + super(message); + this.name = "ProtocolError"; + this.errcode = errcode; + this.errmsg = errmsg; + } +} + +/** + * JSON encoder/decoder for Tryton specific types + */ +class TrytonJSONEncoder { + /** + * Serialize JavaScript objects to JSON with Tryton type handling + * @param {*} obj - Object to serialize + * @returns {string} - JSON string + */ + static serialize(obj) { + return JSON.stringify(obj, (key, value) => { + if (value instanceof Date) { + return { + __class__: "datetime", + year: value.getFullYear(), + month: value.getMonth() + 1, + day: value.getDate(), + hour: value.getHours(), + minute: value.getMinutes(), + second: value.getSeconds(), + microsecond: value.getMilliseconds() * 1000, + }; + } + + if (value instanceof Buffer) { + return { + __class__: "bytes", + base64: value.toString("base64"), + }; + } + + // Handle BigInt as Decimal + if (typeof value === "bigint") { + return { + __class__: "Decimal", + decimal: value.toString(), + }; + } + + return value; + }); + } + + /** + * Deserialize JSON with Tryton type handling + * @param {string} str - JSON string + * @returns {*} - Parsed object + */ + static deserialize(str) { + return JSON.parse(str, (key, value) => { + if (value && typeof value === "object" && value.__class__) { + switch (value.__class__) { + case "datetime": + return new Date( + value.year, + value.month - 1, + value.day, + value.hour || 0, + value.minute || 0, + value.second || 0, + Math.floor((value.microsecond || 0) / 1000) + ); + + case "date": + return new Date(value.year, value.month - 1, value.day); + + case "time": + const today = new Date(); + return new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + value.hour || 0, + value.minute || 0, + value.second || 0, + Math.floor((value.microsecond || 0) / 1000) + ); + + case "timedelta": + // Return seconds as number + return value.seconds || 0; + + case "bytes": + return Buffer.from(value.base64, "base64"); + + case "Decimal": + // Convert to number or keep as string for precision + return parseFloat(value.decimal); + + default: + return value; + } + } + return value; + }); + } +} + +/** + * HTTP Transport for JSON-RPC requests + */ +class Transport { + constructor(options = {}) { + this.fingerprints = options.fingerprints || null; + this.caCerts = options.caCerts || null; + this.session = options.session || null; + this.connection = null; + this.connectTimeout = options.connectTimeout || CONNECT_TIMEOUT; + this.timeout = options.timeout || DEFAULT_TIMEOUT; + this.useHttps = options.useHttps || false; + } + + /** + * Make HTTP request to server + * @param {string} host - Server host + * @param {string} handler - URL path + * @param {string} requestData - JSON request data + * @param {boolean} verbose - Enable verbose logging + * @returns {Promise} - Response object + */ + async request(host, handler, requestData, verbose = false) { + // Detect protocol based on port or explicit protocol + const hostParts = host.split(":"); + const port = hostParts[1] ? parseInt(hostParts[1]) : 80; + const hostname = hostParts[0]; + + // Use HTTPS if explicitly configured, or for standard HTTPS ports + const shouldUseHttps = + this.useHttps || + port === 443 || + port === 8443 || + host.startsWith("https://"); + const protocol = shouldUseHttps ? "https" : "http"; + + const url = new URL(`${protocol}://${host}${handler}`); + const isHttps = url.protocol === "https:"; + + const options = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(requestData), + Connection: "keep-alive", + "Accept-Encoding": "gzip, deflate", + }, + timeout: this.connectTimeout, + // Allow self-signed certificates for testing + rejectUnauthorized: false, + }; + + // Add session authentication + if (this.session) { + const auth = Buffer.from(this.session).toString("base64"); + options.headers["Authorization"] = `Session ${auth}`; + } + + return new Promise((resolve, reject) => { + const client = isHttps ? https : http; + + const req = client.request(options, (res) => { + let data = Buffer.alloc(0); + + res.on("data", (chunk) => { + data = Buffer.concat([data, chunk]); + }); + + res.on("end", () => { + try { + // Handle compression + const encoding = res.headers["content-encoding"]; + let responseText; + + if (encoding === "gzip") { + responseText = zlib + .gunzipSync(data) + .toString("utf-8"); + } else if (encoding === "deflate") { + responseText = zlib + .inflateSync(data) + .toString("utf-8"); + } else { + responseText = data.toString("utf-8"); + } + + if (verbose) { + console.log("Response:", responseText); + } + + const response = + TrytonJSONEncoder.deserialize(responseText); + + // Add cache header if present + const cacheHeader = res.headers["x-tryton-cache"]; + if (cacheHeader) { + try { + response.cache = parseInt(cacheHeader); + } catch (e) { + // Ignore invalid cache header + } + } + + resolve(response); + } catch (error) { + reject( + new ResponseError( + `Failed to parse response: ${error.message}` + ) + ); + } + }); + }); + + req.on("error", (error) => { + if (error.code === "ECONNRESET" || error.code === "EPIPE") { + // Retry once on connection reset + reject( + new ProtocolError( + "Connection reset", + error.code, + error.message + ) + ); + } else { + reject(error); + } + }); + + req.on("timeout", () => { + req.destroy(); + reject(new Error("Request timeout")); + }); + + req.setTimeout(this.timeout); + req.write(requestData); + req.end(); + }); + } + + /** + * Close transport connection + */ + close() { + if (this.connection) { + this.connection.destroy(); + this.connection = null; + } + } +} + +/** + * Server proxy for making RPC calls + */ +class ServerProxy { + constructor(host, port, database = "", options = {}) { + this.host = host; + this.port = port; + this.database = database; + this.verbose = options.verbose || false; + this.handler = database ? `/${encodeURIComponent(database)}/` : "/"; + this.hostUrl = `${host}:${port}`; + this.requestId = 0; + this.cache = options.cache || null; + this.useHttps = options.useHttps || false; + + this.transport = new Transport({ + fingerprints: options.fingerprints, + caCerts: options.caCerts, + session: options.session, + connectTimeout: options.connectTimeout, + timeout: options.timeout, + useHttps: this.useHttps, + }); + } + + /** + * Make RPC request with retry logic + * @param {string} methodName - RPC method name + * @param {Array} params - Method parameters + * @returns {Promise<*>} - Method result + */ + async request(methodName, params) { + this.requestId += 1; + const id = this.requestId; + + const requestData = TrytonJSONEncoder.serialize({ + id: id, + method: methodName, + params: params, + }); + + // Check cache first + if (this.cache && this.cache.cached(methodName)) { + try { + return this.cache.get(methodName, requestData); + } catch (error) { + // Cache miss or expired, continue with request + } + } + + let lastError = null; + + // Retry logic (up to 5 attempts) + for (let attempt = 0; attempt < 5; attempt++) { + try { + const response = await this.transport.request( + this.hostUrl, + this.handler, + requestData, + this.verbose + ); + + // Validate response + if (response.id !== id) { + throw new ResponseError( + `Invalid response id (${response.id}) expected ${id}` + ); + } + + // Handle RPC errors + if (response.error) { + if (this.verbose) { + console.error("RPC Error:", response); + } + throw new Fault(response.error[0], response.error[1] || ""); + } + + // Cache successful response + if (this.cache && response.cache) { + this.cache.set( + methodName, + requestData, + response.cache, + response.result + ); + } + + return response.result; + } catch (error) { + lastError = error; + + // Check if we should retry + if (error instanceof ProtocolError && error.errcode === 503) { + // Service unavailable, wait and retry + const delay = Math.min(attempt + 1, 10) * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); + continue; + } + + // For connection errors, try once more + if ( + attempt === 0 && + (error.code === "ECONNRESET" || + error.code === "EPIPE" || + error instanceof ProtocolError) + ) { + this.transport.close(); + continue; + } + + // Don't retry other errors + break; + } + } + + throw lastError; + } + + /** + * Close server proxy + */ + close() { + this.transport.close(); + } + + /** + * Get SSL status + * @returns {boolean} - Whether connection uses SSL + */ + get ssl() { + return this.port === 443 || this.hostUrl.startsWith("https"); + } + + /** + * Get full URL + * @returns {string} - Full server URL + */ + get url() { + const scheme = this.ssl ? "https" : "http"; + return `${scheme}://${this.hostUrl}${this.handler}`; + } +} + +/** + * Connection pool for reusing ServerProxy instances + */ +class ServerPool { + constructor(host, port, database, options = {}) { + this.host = host; + this.port = port; + this.database = database; + this.options = options; + this.keepMax = options.keepMax || 4; + this.session = options.session || null; + + this.pool = []; + this.used = new Set(); + this.cache = null; + + // Initialize cache if requested + if (options.cache) { + this.cache = new TrytonCache(); + this.options.cache = this.cache; + } + } + + /** + * Get connection from pool or create new one + * @returns {ServerProxy} - Server proxy instance + */ + getConnection() { + let conn; + + if (this.pool.length > 0) { + conn = this.pool.pop(); + } else { + conn = new ServerProxy(this.host, this.port, this.database, { + ...this.options, + cache: this.cache, + }); + } + + this.used.add(conn); + return conn; + } + + /** + * Return connection to pool + * @param {ServerProxy} conn - Connection to return + */ + putConnection(conn) { + this.used.delete(conn); + this.pool.push(conn); + + // Remove excess connections + while (this.pool.length > this.keepMax) { + const oldConn = this.pool.shift(); + oldConn.close(); + } + } + + /** + * Execute callback with a pooled connection + * @param {Function} callback - Async function to execute + * @returns {Promise<*>} - Callback result + */ + async withConnection(callback) { + const conn = this.getConnection(); + try { + return await callback(conn); + } finally { + this.putConnection(conn); + } + } + + /** + * Close all connections in pool + */ + close() { + // Close all pooled connections + for (const conn of this.pool) { + conn.close(); + } + + // Close all used connections + for (const conn of this.used) { + conn.close(); + } + + this.pool = []; + this.used.clear(); + } + + /** + * Clear cache + * @param {string} [prefix] - Optional prefix to clear + */ + clearCache(prefix = null) { + if (this.cache) { + this.cache.clear(prefix); + } + } + + /** + * Get SSL status from any connection + * @returns {boolean|null} - SSL status or null if no connections + */ + get ssl() { + const allConns = [...this.pool, ...this.used]; + if (allConns.length > 0) { + return allConns[0].ssl; + } + return null; + } + + /** + * Get URL from any connection + * @returns {string|null} - URL or null if no connections + */ + get url() { + const allConns = [...this.pool, ...this.used]; + if (allConns.length > 0) { + return allConns[0].url; + } + return null; + } +} + +module.exports = { + ResponseError, + Fault, + ProtocolError, + TrytonJSONEncoder, + Transport, + ServerProxy, + ServerPool, +};