Files

578 lines
17 KiB
TypeScript

/**
* Tryton RPC Client for Node.js
* TypeScript implementation of sabatron-tryton-rpc-client
*/
import { ServerProxy, ServerPool } from "./jsonrpc";
import { ServerProxyOptions, ServerPoolOptions } from "./types";
// Constants
const CONNECT_TIMEOUT = 5000; // 5 seconds
const DEFAULT_TIMEOUT = 30000; // 30 seconds
import {
TrytonClientConfig,
TrytonClientOptions,
SearchDomain,
TrytonContext,
TrytonRecord,
TrytonUser,
UserPreferences,
LoginResult,
RecordId,
RecordIds,
FieldName,
ModelName,
ClientConfig,
TrytonMethodCall,
TypedModelOperations,
} from "./types";
/**
* Main client class for connecting to Tryton server via RPC
*/
export class TrytonClient {
private hostname: string;
private database: string;
private username: string;
private password: string;
private port: number;
private language: string;
private options: TrytonClientOptions;
private useHttps: boolean;
private connection: ServerPool | null;
private session: string | null;
/**
* Create a new Tryton client
*/
constructor(config: TrytonClientConfig) {
const {
hostname,
database,
username,
password,
port = 8000,
language = "en",
options = {},
} = config;
// Extract protocol from hostname if present
if (hostname.startsWith("https://")) {
this.hostname = hostname.replace("https://", "");
this.useHttps = true;
} else if (hostname.startsWith("http://")) {
this.hostname = hostname.replace("http://", "");
this.useHttps = false;
} else {
this.hostname = hostname;
this.useHttps = port === 443 || port === 8443;
}
this.database = database;
this.username = username;
this.password = password;
this.port = port;
this.language = language;
this.options = options;
this.connection = null;
this.session = null;
}
/**
* Alternative constructor for backward compatibility
*/
static create(
hostname: string,
database: string,
username: string,
password: string,
port: number = 8000,
language: string = "en"
): TrytonClient {
return new TrytonClient({
hostname,
database,
username,
password,
port,
language,
});
}
/**
* Connect to Tryton server and authenticate
* @returns {Promise<boolean>} - True if connection successful
* @throws {Error} - If connection or authentication fails
*/
async connect(): Promise<boolean> {
try {
const proxy = new ServerProxy(
this.hostname,
this.port,
this.database,
{
verbose: this.options.verbose || false,
connectTimeout:
this.options.connectTimeout || CONNECT_TIMEOUT,
timeout: this.options.timeout || DEFAULT_TIMEOUT,
useHttps: this.useHttps,
}
);
// Perform login
const parameters = {
password: this.password,
};
const result = await proxy.request<LoginResult>("common.db.login", [
this.username,
parameters,
this.language,
]);
proxy.close();
this.session = [this.username, ...result].join(":");
// Create connection pool with session
this.connection = new ServerPool(
this.hostname,
this.port,
this.database,
{
session: this.session,
cache: this.options.cache !== false ? [] : null, // Enable cache by default
verbose: this.options.verbose || false,
connectTimeout:
this.options.connectTimeout || CONNECT_TIMEOUT,
timeout: this.options.timeout || DEFAULT_TIMEOUT,
keepMax: this.options.keepMax || 4,
useHttps: this.useHttps,
}
);
return true;
} catch (error) {
throw new Error(`Connection failed: ${(error as Error).message}`);
}
}
/**
* Call RPC method on server
* @param {string} methodName - RPC method name (e.g., 'model.party.party.read')
* @param {Array} args - Method arguments
* @returns {Promise<*>} - Method result
* @throws {Error} - If not connected or method call fails
*/
async call<T = any>(methodName: string, args: any[] = []): Promise<T> {
if (!this.connection) {
throw new Error("Not connected. Call connect() first.");
}
return this.connection.withConnection(async (conn) => {
return conn.request<T>(methodName, args);
});
}
/**
* Call multiple RPC methods in sequence
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callMultiple(calls: TrytonMethodCall[]): Promise<any[]> {
const results: any[] = [];
for (const call of calls) {
const result = await this.call(call.method, call.args);
results.push(result);
}
return results;
}
/**
* Call multiple RPC methods in parallel
* @param {Array<{method: string, args: Array}>} calls - Array of method calls
* @returns {Promise<Array>} - Array of results
*/
async callParallel(calls: TrytonMethodCall[]): Promise<any[]> {
const promises = calls.map((call) => this.call(call.method, call.args));
return Promise.all(promises);
}
/**
* Helper method to read records from a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to read
* @param {Array<string>} fields - Fields to read
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async read<T extends TrytonRecord = TrytonRecord>(
model: ModelName,
ids: RecordIds,
fields: FieldName[],
context: TrytonContext = {}
): Promise<T[]> {
return this.call<T[]>(`model.${model}.read`, [ids, fields, context]);
}
/**
* Helper method to create records in a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<Object>} records - Records to create
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of created record IDs
*/
async create(
model: ModelName,
records: Record<string, any>[],
context: TrytonContext = {}
): Promise<RecordIds> {
return this.call<RecordIds>(`model.${model}.create`, [
records,
context,
]);
}
/**
* Helper method to write/update records in a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to update
* @param {Object} values - Values to update
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async write(
model: ModelName,
ids: RecordIds,
values: Record<string, any>,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`model.${model}.write`, [ids, values, context]);
}
/**
* Helper method to delete records from a model
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array<number>} ids - Record IDs to delete
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<void>}
*/
async delete(
model: ModelName,
ids: RecordIds,
context: TrytonContext = {}
): Promise<void> {
return this.call<void>(`model.${model}.delete`, [ids, context]);
}
/**
* Helper method to search for records
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {number} [offset=0] - Offset for pagination
* @param {number} [limit=null] - Limit for pagination
* @param {Array<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array<number>>} - Array of record IDs
*/
async search(
model: ModelName,
domain: SearchDomain,
offset: number = 0,
limit: number | null = null,
order: string[] | null = null,
context: TrytonContext = {}
): Promise<RecordIds> {
return this.call<RecordIds>(`model.${model}.search`, [
domain,
offset,
limit,
order,
context,
]);
}
/**
* Helper method to search and read records in one call
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {Array<string>} fields - Fields to read
* @param {number} [offset=0] - Offset for pagination
* @param {number} [limit=null] - Limit for pagination
* @param {Array<string>} [order=null] - Order specification
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<Array>} - Array of records
*/
async searchRead<T extends TrytonRecord = TrytonRecord>(
model: ModelName,
domain: SearchDomain,
fields: FieldName[],
offset: number = 0,
limit: number | null = null,
order: string[] | null = null,
context: TrytonContext = {}
): Promise<T[]> {
return this.call<T[]>(`model.${model}.search_read`, [
domain,
offset,
limit,
order,
fields,
context,
]);
}
/**
* Helper method to count records
* @param {string} model - Model name (e.g., 'party.party')
* @param {Array} domain - Search domain
* @param {Object} [context={}] - Context dictionary
* @returns {Promise<number>} - Number of records
*/
async searchCount(
model: ModelName,
domain: SearchDomain,
context: TrytonContext = {}
): Promise<number> {
return this.call<number>(`model.${model}.search_count`, [
domain,
context,
]);
}
/**
* Get database information
* @returns {Promise<Object>} - Database info
*/
async getDatabaseInfo(): Promise<any> {
return this.call("common.db.get_info", []);
}
/**
* List available databases
* @returns {Promise<Array<string>>} - Database names
*/
async listDatabases(): Promise<string[]> {
return this.call<string[]>("common.db.list", []);
}
/**
* Get server version
* @returns {Promise<string>} - Server version
*/
async getVersion(): Promise<string> {
return this.call<string>("common.version", []);
}
/**
* Get user preferences
*/
async getUserPreferences(): Promise<UserPreferences> {
return this.call<UserPreferences>("model.res.user.get_preferences", [
true,
{},
]);
}
/**
* Get current user information
*/
async getCurrentUser(): Promise<TrytonUser> {
const preferences = await this.getUserPreferences();
const users = await this.read<TrytonUser>(
"res.user",
[preferences.id],
["id", "name", "login", "language", "company", "email"]
);
if (!users || users.length === 0) {
throw new Error("User not found");
}
return users[0]!;
}
/**
* Clear cache for specific prefix or all
* @param {string} [prefix] - Optional prefix to clear
*/
clearCache(prefix?: string): void {
if (this.connection) {
this.connection.clearCache(prefix);
}
}
/**
* Get connection SSL status
* @returns {boolean|null} - SSL status or null if not connected
*/
get ssl(): boolean | null {
return this.connection ? this.connection.ssl : null;
}
/**
* Get connection URL
* @returns {string|null} - Connection URL or null if not connected
*/
get url(): string | null {
return this.connection ? this.connection.url : null;
}
/**
* Check if client is connected
* @returns {boolean} - True if connected
*/
get isConnected(): boolean {
return this.connection !== null;
}
/**
* Get current session
* @returns {string|null} - Session string or null if not connected
*/
getSession(): string | null {
return this.session;
}
/**
* Get current user ID from session
*/
getUserId(): number | null {
if (!this.session) return null;
const parts = this.session.split(":");
return parts.length >= 2 ? parseInt(parts[1], 10) : null;
}
/**
* Get session token from session
*/
getSessionToken(): string | null {
if (!this.session) return null;
const parts = this.session.split(":");
return parts.length >= 3 ? parts[2] : null;
}
/**
* Get username
*/
getUsername(): string {
return this.username;
}
/**
* Close connection and cleanup resources
*/
close(): void {
if (this.connection) {
this.connection.close();
this.connection = null;
this.session = null;
}
}
/**
* Create a new client instance with the same configuration
* @returns {TrytonClient} - New client instance
*/
clone(): TrytonClient {
return new TrytonClient({
hostname: this.hostname,
database: this.database,
username: this.username,
password: this.password,
port: this.port,
language: this.language,
options: { ...this.options },
});
}
/**
* Get client configuration (without sensitive data)
* @returns {Object} - Client configuration
*/
getConfig(): ClientConfig {
return {
hostname: this.hostname,
database: this.database,
username: this.username,
port: this.port,
language: this.language,
isConnected: this.isConnected,
ssl: this.ssl,
url: this.url,
};
}
/**
* Type-safe model operations factory
* Creates a typed interface for specific models
*/
model<T extends TrytonRecord = TrytonRecord>(
modelName: ModelName
): TypedModelOperations<T> {
return {
read: (
ids: RecordIds,
fields: FieldName[],
context?: TrytonContext
) => this.read<T>(modelName, ids, fields, context),
create: (
records: Partial<Omit<T, "id">>[],
context?: TrytonContext
) =>
this.create(
modelName,
records as Record<string, any>[],
context
),
write: (
ids: RecordIds,
values: Partial<Omit<T, "id">>,
context?: TrytonContext
) =>
this.write(
modelName,
ids,
values as Record<string, any>,
context
),
delete: (ids: RecordIds, context?: TrytonContext) =>
this.delete(modelName, ids, context),
search: (
domain: SearchDomain,
offset?: number,
limit?: number,
order?: string[],
context?: TrytonContext
) => this.search(modelName, domain, offset, limit, order, context),
searchRead: (
domain: SearchDomain,
fields: FieldName[],
offset?: number,
limit?: number,
order?: string[],
context?: TrytonContext
) =>
this.searchRead<T>(
modelName,
domain,
fields,
offset,
limit,
order,
context
),
searchCount: (domain: SearchDomain, context?: TrytonContext) =>
this.searchCount(modelName, domain, context),
};
}
}
// Export for CommonJS compatibility
export default TrytonClient;